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 void reject_non_empty_sfcw_mode(const nlohmann::json& comp_json, const std::string& owner)
196 {
197 if (comp_json.contains("sfcw_mode") &&
198 (!comp_json.at("sfcw_mode").is_object() || !comp_json.at("sfcw_mode").empty()))
199 {
200 throw std::runtime_error(owner + " sfcw_mode must be an empty object.");
201 }
202 }
203
204 std::optional<RealType> get_optional_positive_real(const nlohmann::json& object, const std::string_view key,
205 const std::string& owner)
206 {
207 const std::string key_string(key);
208 if (!object.contains(key_string))
209 {
210 return std::nullopt;
211 }
212 const RealType value = object.at(key_string).get<RealType>();
213 if (value <= 0.0 || !std::isfinite(value))
214 {
215 throw std::runtime_error(owner + " " + key_string + " must be a finite positive value.");
216 }
217 return value;
218 }
219
221 {
222 return if_chain.sample_rate_hz.has_value() || if_chain.filter_bandwidth_hz.has_value() ||
223 if_chain.filter_transition_width_hz.has_value();
224 }
225
226 void reject_disabled_json_dechirp_fields(const nlohmann::json& mode_json,
228 const std::string& owner)
229 {
230 if (mode_json.contains("dechirp_reference"))
231 {
232 throw std::runtime_error(owner + " declares dechirp_reference while dechirp_mode is 'none'.");
233 }
235 {
236 throw std::runtime_error(owner + " declares IF-chain fields while dechirp_mode is 'none'.");
237 }
238 }
239
241 {
242 if ((if_chain.filter_bandwidth_hz.has_value() || if_chain.filter_transition_width_hz.has_value()) &&
243 !if_chain.sample_rate_hz.has_value())
244 {
245 throw std::runtime_error(owner + " IF filter fields require if_sample_rate.");
246 }
247 if (if_chain.sample_rate_hz.has_value())
248 {
250 if (*if_chain.sample_rate_hz > sim_rate)
251 {
252 throw std::runtime_error(owner + " if_sample_rate must not exceed the simulation sample rate.");
253 }
254 }
255 if (if_chain.sample_rate_hz.has_value() && if_chain.filter_bandwidth_hz.has_value() &&
256 *if_chain.filter_bandwidth_hz >= *if_chain.sample_rate_hz / 2.0)
257 {
258 throw std::runtime_error(owner + " if_filter_bandwidth must be less than half if_sample_rate.");
259 }
260 }
261
262 std::string required_non_empty_json_string(const nlohmann::json& object, const std::string_view key,
263 const std::string& owner, const std::string_view context)
264 {
265 const std::string key_string(key);
266 auto value = object.at(key_string).get<std::string>();
267 if (value.empty())
268 {
269 throw std::runtime_error(owner + " " + std::string(context) + " has an empty " + key_string + ".");
270 }
271 return value;
272 }
273
275 const std::string& owner)
276 {
277 if (!mode_json.contains("dechirp_reference") || !mode_json.at("dechirp_reference").is_object())
278 {
279 throw std::runtime_error(owner + " enables dechirping but does not declare dechirp_reference.");
280 }
281
282 const auto& ref_json = mode_json.at("dechirp_reference");
283 reject_unknown_keys(ref_json, owner, "dechirp_reference", {"source", "transmitter_name", "waveform_name"});
284 if (!ref_json.contains("source"))
285 {
286 throw std::runtime_error(owner + " dechirp_reference requires source.");
287 }
288
290 reference.source = radar::parseDechirpReferenceSourceToken(ref_json.at("source").get<std::string>());
291 const bool has_transmitter_name = ref_json.contains("transmitter_name");
292 const bool has_waveform_name = ref_json.contains("waveform_name");
293
294 switch (reference.source)
295 {
298 {
299 throw std::runtime_error(owner +
300 " attached dechirp_reference must not set transmitter_name or waveform_name.");
301 }
302 break;
305 {
306 throw std::runtime_error(owner + " transmitter dechirp_reference requires transmitter_name only.");
307 }
308 reference.name =
309 required_non_empty_json_string(ref_json, "transmitter_name", owner, "transmitter dechirp_reference");
310 break;
313 {
314 throw std::runtime_error(owner + " custom dechirp_reference requires waveform_name only.");
315 }
316 reference.name =
317 required_non_empty_json_string(ref_json, "waveform_name", owner, "custom dechirp_reference");
318 break;
320 throw std::runtime_error(owner + " dechirp_reference source must be attached, transmitter, or custom.");
321 }
322
323 return reference;
324 }
325
326 /// Parses receiver-side dechirp settings from a JSON component.
328 const std::string& owner)
329 {
331 {
333 return;
334 }
335
336 const auto& mode_json = comp_json.contains("fmcw_mode") ? comp_json.at("fmcw_mode") : nlohmann::json::object();
337 if (!mode_json.is_object())
338 {
339 throw std::runtime_error(owner + " fmcw_mode must be an object.");
340 }
341 reject_unknown_keys(mode_json, owner, "fmcw_mode",
342 {"dechirp_mode", "dechirp_reference", "if_sample_rate", "if_filter_bandwidth",
343 "if_filter_transition_width"});
344
346 if (mode_json.contains("dechirp_mode"))
347 {
348 mode = radar::parseDechirpModeToken(mode_json.at("dechirp_mode").get<std::string>());
349 }
350
352 .sample_rate_hz = get_optional_positive_real(mode_json, "if_sample_rate", owner),
353 .filter_bandwidth_hz = get_optional_positive_real(mode_json, "if_filter_bandwidth", owner),
354 .filter_transition_width_hz = get_optional_positive_real(mode_json, "if_filter_transition_width", owner)};
355
357 {
359 receiver.setDechirpMode(mode);
360 return;
361 }
362
365
366 receiver.setDechirpMode(mode);
367 receiver.setDechirpReference(std::move(reference));
368 receiver.setFmcwIfChainRequest(if_chain);
369 }
370
371 /// Serializes receiver-side FMCW mode settings.
373 {
374 nlohmann::json mode_json = nlohmann::json::object();
375 if (!receiver.isDechirpEnabled())
376 {
377 return mode_json;
378 }
379
380 const auto& reference = receiver.getDechirpReference();
381 mode_json["dechirp_mode"] = std::string(radar::dechirpModeToken(receiver.getDechirpMode()));
382 const auto& if_chain = receiver.getFmcwIfChainRequest();
383 if (if_chain.sample_rate_hz.has_value())
384 {
385 mode_json["if_sample_rate"] = *if_chain.sample_rate_hz;
386 }
387 if (if_chain.filter_bandwidth_hz.has_value())
388 {
389 mode_json["if_filter_bandwidth"] = *if_chain.filter_bandwidth_hz;
390 }
391 if (if_chain.filter_transition_width_hz.has_value())
392 {
393 mode_json["if_filter_transition_width"] = *if_chain.filter_transition_width_hz;
394 }
395 nlohmann::json ref_json = {{"source", std::string(radar::dechirpReferenceSourceToken(reference.source))}};
397 {
398 ref_json["transmitter_name"] =
399 !reference.transmitter_name.empty() ? reference.transmitter_name : reference.name;
400 }
402 {
403 ref_json["waveform_name"] = !reference.waveform_name.empty() ? reference.waveform_name : reference.name;
404 }
405 mode_json["dechirp_reference"] = std::move(ref_json);
406 return mode_json;
407 }
408}
409
410namespace math
411{
412 void to_json(nlohmann::json& j, const Vec3& v) // NOLINT(*-use-internal-linkage)
413 {
414 j = {{"x", v.x}, {"y", v.y}, {"z", v.z}};
415 } // NOLINT(*-use-internal-linkage)
416
417 void from_json(const nlohmann::json& j, Vec3& v) // NOLINT(*-use-internal-linkage)
418 {
419 j.at("x").get_to(v.x);
420 j.at("y").get_to(v.y);
421 j.at("z").get_to(v.z);
422 }
423
424 void to_json(nlohmann::json& j, const Coord& c) // NOLINT(*-use-internal-linkage)
425 {
426 j = {{"time", c.t}, {"x", c.pos.x}, {"y", c.pos.y}, {"altitude", c.pos.z}};
427 }
428
429 void from_json(const nlohmann::json& j, Coord& c) // NOLINT(*-use-internal-linkage)
430 {
431 j.at("time").get_to(c.t);
432 j.at("x").get_to(c.pos.x);
433 j.at("y").get_to(c.pos.y);
434 j.at("altitude").get_to(c.pos.z);
435 }
436
437 void to_json(nlohmann::json& j, const RotationCoord& rc) // NOLINT(*-use-internal-linkage)
438 {
439 const auto unit = params::rotationAngleUnit();
440 j = {{"time", rc.t},
443 }
444
445 void from_json(const nlohmann::json& j, RotationCoord& rc) // NOLINT(*-use-internal-linkage)
446 {
447 j.at("time").get_to(rc.t);
449 j.at("azimuth").get<RealType>(), j.at("elevation").get<RealType>(), rc.t, params::rotationAngleUnit());
450 rc.azimuth = external.azimuth;
451 rc.elevation = external.elevation;
452 }
453
458
459 void to_json(nlohmann::json& j, const Path& p) // NOLINT(*-use-internal-linkage)
460 {
461 j = {{"interpolation", p.getType()}, {"positionwaypoints", p.getCoords()}};
462 }
463
464 void from_json(const nlohmann::json& j, Path& p) // NOLINT(*-use-internal-linkage)
465 {
466 p.setInterp(j.at("interpolation").get<Path::InterpType>());
467 for (const auto waypoints = j.at("positionwaypoints").get<std::vector<Coord>>(); const auto& wp : waypoints)
468 {
469 p.addCoord(wp);
470 }
471 p.finalize();
472 }
473
477 "constant"}, // Not used in xml_parser or UI yet, but for completeness
480
481 void to_json(nlohmann::json& j, const RotationPath& p) // NOLINT(*-use-internal-linkage)
482 {
483 j["interpolation"] = p.getType();
484 // This logic exists to map the two different rotation definitions from the
485 // XML schema (<fixedrotation> and <rotationpath>) into a unified JSON
486 // structure that the frontend can more easily handle.
488 {
489 // A constant-rate rotation path corresponds to the <fixedrotation> XML element.
490 // The start and rate values are converted to compass degrees per second.
491 // No normalization is applied to preserve negative start angles.
492 const auto unit = params::rotationAngleUnit();
493 j["startazimuth"] = serial::rotation_angle_utils::internal_azimuth_to_external(p.getStart().azimuth, unit);
494 j["startelevation"] =
496 j["azimuthrate"] =
498 j["elevationrate"] =
500 }
501 else
502 {
503 j["rotationwaypoints"] = p.getCoords();
504 }
505 }
506
507 void from_json(const nlohmann::json& j, RotationPath& p) // NOLINT(*-use-internal-linkage)
508 {
509 p.setInterp(j.at("interpolation").get<RotationPath::InterpType>());
510 for (const auto waypoints = j.at("rotationwaypoints").get<std::vector<RotationCoord>>();
511 const auto& wp : waypoints)
512 {
513 p.addCoord(wp);
514 }
515 p.finalize();
516 }
517
518}
519
520namespace timing
521{
522 void to_json(nlohmann::json& j, const PrototypeTiming& pt) // NOLINT(*-use-internal-linkage)
523 {
524 j = nlohmann::json{{"id", sim_id_to_json(pt.getId())},
525 {"name", pt.getName()},
526 {"frequency", pt.getFrequency()},
527 {"synconpulse", pt.getSyncOnPulse()}};
528
529 if (pt.getFreqOffset().has_value())
530 {
531 j["freq_offset"] = pt.getFreqOffset().value();
532 }
533 if (pt.getRandomFreqOffsetStdev().has_value())
534 {
535 j["random_freq_offset_stdev"] = pt.getRandomFreqOffsetStdev().value();
536 }
537 if (pt.getPhaseOffset().has_value())
538 {
539 j["phase_offset"] = pt.getPhaseOffset().value();
540 }
541 if (pt.getRandomPhaseOffsetStdev().has_value())
542 {
543 j["random_phase_offset_stdev"] = pt.getRandomPhaseOffsetStdev().value();
544 }
545
546 std::vector<RealType> alphas;
547 std::vector<RealType> weights;
548 pt.copyAlphas(alphas, weights);
549 if (!alphas.empty())
550 {
551 nlohmann::json noise_entries = nlohmann::json::array();
552 for (size_t i = 0; i < alphas.size(); ++i)
553 {
554 noise_entries.push_back({{"alpha", alphas[i]}, {"weight", weights[i]}});
555 }
556 j["noise_entries"] = noise_entries;
557 }
558 }
559
560 void from_json(const nlohmann::json& j, PrototypeTiming& pt) // NOLINT(*-use-internal-linkage)
561 {
562 pt.setFrequency(j.at("frequency").get<RealType>());
563 if (j.value("synconpulse", false))
564 {
565 pt.setSyncOnPulse();
566 }
567 else
568 {
569 pt.clearSyncOnPulse();
570 }
571
572 if (j.contains("freq_offset"))
573 {
574 pt.setFreqOffset(j.at("freq_offset").get<RealType>());
575 }
576 else
577 pt.clearFreqOffset();
578 if (j.contains("random_freq_offset_stdev"))
579 {
580 pt.setRandomFreqOffsetStdev(j.at("random_freq_offset_stdev").get<RealType>());
581 }
582 else
583 pt.clearRandomFreqOffsetStdev();
584 if (j.contains("phase_offset"))
585 {
586 pt.setPhaseOffset(j.at("phase_offset").get<RealType>());
587 }
588 else
589 pt.clearPhaseOffset();
590 if (j.contains("random_phase_offset_stdev"))
591 {
592 pt.setRandomPhaseOffsetStdev(j.at("random_phase_offset_stdev").get<RealType>());
593 }
594 else
595 pt.clearRandomPhaseOffsetStdev();
596
597 pt.clearNoiseEntries();
598 if (j.contains("noise_entries"))
599 {
600 for (const auto& entry : j.at("noise_entries"))
601 {
602 pt.setAlpha(entry.at("alpha").get<RealType>(), entry.at("weight").get<RealType>());
603 }
604 }
605 }
606}
607
608namespace fers_signal
609{
610 void to_json(nlohmann::json& j, const RadarSignal& rs) // NOLINT(*-use-internal-linkage)
611 {
612 j = nlohmann::json{{"id", sim_id_to_json(rs.getId())},
613 {"name", rs.getName()},
614 {"power", rs.getPower()},
615 {"carrier_frequency", rs.getCarrier()}};
616 if (const auto* file = rs.getFileSignal(); file != nullptr)
617 {
618 std::string_view key = "pulsed_from_file";
619 if (file->getKind() == FileWaveformKind::Cw)
620 {
621 key = "cw_from_file";
622 }
623 else if (file->getKind() == FileWaveformKind::Fmcw)
624 {
625 key = "fmcw_from_file";
626 }
627 if (const auto& filename = rs.getFilename(); filename.has_value())
628 {
629 j[key] = {{"filename", *filename}};
630 }
631 else
632 {
633 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
634 "' without a source filename.");
635 }
636 }
637 else if (dynamic_cast<const CwSignal*>(rs.getSignal()) != nullptr)
638 {
639 j["cw"] = nlohmann::json::object();
640 }
641 else if (const auto* sfcw = rs.getSteppedFrequencySignal(); sfcw != nullptr)
642 {
643 j["stepped_frequency"] = {{"start_frequency_offset", sfcw->getStartFrequencyOffset()},
644 {"step_size", sfcw->getStepSize()},
645 {"step_count", sfcw->getStepCount()},
646 {"dwell_time", sfcw->getDwellTime()},
647 {"step_period", sfcw->getStepPeriod()}};
648 if (sfcw->getSweepCount().has_value())
649 {
650 j["stepped_frequency"]["sweep_count"] = *sfcw->getSweepCount();
651 }
652 }
653 else if (const auto* fmcw = rs.getFmcwChirpSignal(); fmcw != nullptr)
654 {
655 j["fmcw_linear_chirp"] = {{"direction", std::string(fmcwChirpDirectionToken(fmcw->getDirection()))},
656 {"chirp_bandwidth", fmcw->getChirpBandwidth()},
657 {"chirp_duration", fmcw->getChirpDuration()},
658 {"chirp_period", fmcw->getChirpPeriod()}};
659 if (std::abs(fmcw->getStartFrequencyOffset()) > EPSILON)
660 {
661 j["fmcw_linear_chirp"]["start_frequency_offset"] = fmcw->getStartFrequencyOffset();
662 }
663 if (fmcw->getChirpCount().has_value())
664 {
665 j["fmcw_linear_chirp"]["chirp_count"] = *fmcw->getChirpCount();
666 }
667 }
668 else if (const auto* triangle = rs.getFmcwTriangleSignal(); triangle != nullptr)
669 {
670 j["fmcw_triangle"] = {{"chirp_bandwidth", triangle->getChirpBandwidth()},
671 {"chirp_duration", triangle->getChirpDuration()}};
672 if (std::abs(triangle->getStartFrequencyOffset()) > EPSILON)
673 {
674 j["fmcw_triangle"]["start_frequency_offset"] = triangle->getStartFrequencyOffset();
675 }
676 if (triangle->getTriangleCount().has_value())
677 {
678 j["fmcw_triangle"]["triangle_count"] = *triangle->getTriangleCount();
679 }
680 }
681 else
682 {
683 if (const auto& filename = rs.getFilename(); filename.has_value())
684 {
685 j["pulsed_from_file"] = {{"filename", *filename}};
686 }
687 else
688 {
689 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
690 "' without a source filename.");
691 }
692 }
693 }
694
695 void from_json(const nlohmann::json& j, std::unique_ptr<RadarSignal>& rs) // NOLINT(*-use-internal-linkage)
696 {
697 const auto name = j.at("name").get<std::string>();
698 const auto id = parse_json_id(j, "id", "waveform");
699 const auto power = j.at("power").get<RealType>();
700 const auto carrier = j.at("carrier_frequency").get<RealType>();
701
702 if (j.contains("cw"))
703 {
704 auto cw_signal = std::make_unique<CwSignal>();
705 rs = std::make_unique<RadarSignal>(name, power, carrier, params::endTime() - params::startTime(),
706 std::move(cw_signal), id);
707 }
708 else if (j.contains("stepped_frequency"))
709 {
710 const auto& sfcw_json = j.at("stepped_frequency");
711 const auto step_count = sfcw_json.at("step_count").get<long long>();
712 if (step_count <= 0)
713 {
714 throw std::runtime_error("Waveform '" + name + "' has an invalid step_count.");
715 }
716 std::optional<std::size_t> sweep_count;
717 if (sfcw_json.contains("sweep_count"))
718 {
719 const auto parsed_count = sfcw_json.at("sweep_count").get<long long>();
720 if (parsed_count <= 0)
721 {
722 throw std::runtime_error("Waveform '" + name + "' has an invalid sweep_count.");
723 }
724 sweep_count = static_cast<std::size_t>(parsed_count);
725 }
726 auto sfcw_signal = std::make_unique<SteppedFrequencySignal>(
727 sfcw_json.at("start_frequency_offset").get<RealType>(), sfcw_json.at("step_size").get<RealType>(),
728 static_cast<std::size_t>(step_count), sfcw_json.at("dwell_time").get<RealType>(),
729 sfcw_json.at("step_period").get<RealType>(), sweep_count);
730 rs = std::make_unique<RadarSignal>(name, power, carrier, sfcw_signal->getDwellTime(),
731 std::move(sfcw_signal), id);
732 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
733 }
734 else if (j.contains("fmcw_linear_chirp"))
735 {
736 const auto& fmcw_json = j.at("fmcw_linear_chirp");
737 const auto direction = parseFmcwChirpDirection(fmcw_json.at("direction").get<std::string>());
738 std::optional<std::size_t> chirp_count;
739 if (fmcw_json.contains("chirp_count"))
740 {
741 const auto parsed_count = fmcw_json.at("chirp_count").get<long long>();
742 if (parsed_count <= 0)
743 {
744 throw std::runtime_error("Waveform '" + name + "' has an invalid chirp_count.");
745 }
746 chirp_count = static_cast<std::size_t>(parsed_count);
747 }
748
749 auto fmcw_signal = std::make_unique<FmcwChirpSignal>(
750 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
751 fmcw_json.at("chirp_period").get<RealType>(), fmcw_json.value("start_frequency_offset", 0.0),
752 chirp_count, direction);
753 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getChirpDuration(),
754 std::move(fmcw_signal), id);
755 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
756 }
757 else if (j.contains("fmcw_triangle"))
758 {
759 const auto& fmcw_json = j.at("fmcw_triangle");
760 std::optional<std::size_t> triangle_count;
761 if (fmcw_json.contains("triangle_count"))
762 {
763 const auto& count_json = fmcw_json.at("triangle_count");
764 if (!count_json.is_number_integer() && !count_json.is_number_unsigned())
765 {
766 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
767 }
768 const auto parsed_count = count_json.get<long long>();
769 if (parsed_count <= 0)
770 {
771 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
772 }
773 triangle_count = static_cast<std::size_t>(parsed_count);
774 }
775
776 auto fmcw_signal = std::make_unique<FmcwTriangleSignal>(
777 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
778 fmcw_json.value("start_frequency_offset", 0.0), triangle_count);
779 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getTrianglePeriod(),
780 std::move(fmcw_signal), id);
781 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
782 }
783 else if (j.contains("pulsed_from_file"))
784 {
785 const auto& pulsed_file = j.at("pulsed_from_file");
786 const auto filename = pulsed_file.value("filename", "");
787 if (filename.empty())
788 {
789 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
790 return; // rs remains nullptr
791 }
792 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id);
793 }
794 else if (j.contains("cw_from_file"))
795 {
796 const auto filename = j.at("cw_from_file").value("filename", "");
797 if (filename.empty())
798 {
799 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
800 return;
801 }
803 }
804 else if (j.contains("fmcw_from_file"))
805 {
806 const auto filename = j.at("fmcw_from_file").value("filename", "");
807 if (filename.empty())
808 {
809 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
810 return;
811 }
813 }
814 else
815 {
816 throw std::runtime_error("Unsupported waveform type in from_json for '" + name + "'");
817 }
818 }
819}
820
821namespace antenna
822{
823 void to_json(nlohmann::json& j, const Antenna& a) // NOLINT(*-use-internal-linkage)
824 {
825 j = {{"id", sim_id_to_json(a.getId())}, {"name", a.getName()}, {"efficiency", a.getEfficiencyFactor()}};
826
827 if (const auto* sinc = dynamic_cast<const Sinc*>(&a))
828 {
829 j["pattern"] = "sinc";
830 j["alpha"] = sinc->getAlpha();
831 j["beta"] = sinc->getBeta();
832 j["gamma"] = sinc->getGamma();
833 }
834 else if (const auto* gaussian = dynamic_cast<const Gaussian*>(&a))
835 {
836 j["pattern"] = "gaussian";
837 j["azscale"] = gaussian->getAzimuthScale();
838 j["elscale"] = gaussian->getElevationScale();
839 }
840 else if (const auto* sh = dynamic_cast<const SquareHorn*>(&a))
841 {
842 j["pattern"] = "squarehorn";
843 j["diameter"] = sh->getDimension();
844 }
845 else if (const auto* parabolic = dynamic_cast<const Parabolic*>(&a))
846 {
847 j["pattern"] = "parabolic";
848 j["diameter"] = parabolic->getDiameter();
849 }
850 else if (const auto* xml = dynamic_cast<const XmlAntenna*>(&a))
851 {
852 j["pattern"] = "xml";
853 j["filename"] = xml->getFilename();
854 }
855 else if (const auto* h5 = dynamic_cast<const H5Antenna*>(&a))
856 {
857 j["pattern"] = "file";
858 j["filename"] = h5->getFilename();
859 }
860 else
861 {
862 j["pattern"] = "isotropic";
863 }
864 }
865
866 void from_json(const nlohmann::json& j, std::unique_ptr<Antenna>& ant) // NOLINT(*-use-internal-linkage)
867 {
868 const auto name = j.at("name").get<std::string>();
869 const auto id = parse_json_id(j, "id", "Antenna");
870 const auto pattern = j.value("pattern", "isotropic");
871
872 if (pattern == "isotropic")
873 {
874 ant = std::make_unique<Isotropic>(name, id);
875 }
876 else if (pattern == "sinc")
877 {
878 ant = std::make_unique<Sinc>(name, j.at("alpha").get<RealType>(), j.at("beta").get<RealType>(),
879 j.at("gamma").get<RealType>(), id);
880 }
881 else if (pattern == "gaussian")
882 {
883 ant =
884 std::make_unique<Gaussian>(name, j.at("azscale").get<RealType>(), j.at("elscale").get<RealType>(), id);
885 }
886 else if (pattern == "squarehorn")
887 {
888 ant = std::make_unique<SquareHorn>(name, j.at("diameter").get<RealType>(), id);
889 }
890 else if (pattern == "parabolic")
891 {
892 ant = std::make_unique<Parabolic>(name, j.at("diameter").get<RealType>(), id);
893 }
894 else if (pattern == "xml")
895 {
896 const auto filename = j.value("filename", "");
897 if (filename.empty())
898 {
899 LOG(logging::Level::WARNING, "Skipping load of XML antenna '{}': filename is empty.", name);
900 return; // ant remains nullptr
901 }
902 ant = std::make_unique<XmlAntenna>(name, filename, id);
903 }
904 else if (pattern == "file")
905 {
906 const auto filename = j.value("filename", "");
907 if (filename.empty())
908 {
909 LOG(logging::Level::WARNING, "Skipping load of H5 antenna '{}': filename is empty.", name);
910 return; // ant remains nullptr
911 }
912 ant = std::make_unique<H5Antenna>(name, filename, id);
913 }
914 else
915 {
916 throw std::runtime_error("Unsupported antenna pattern in from_json: " + pattern);
917 }
918
919 ant->setEfficiencyFactor(j.value("efficiency", 1.0));
920 }
921}
922
923namespace radar
924{
925 void to_json(nlohmann::json& j, const SchedulePeriod& p) // NOLINT(*-use-internal-linkage)
926 {
927 j = {{"start", p.start}, {"end", p.end}};
928 } // NOLINT(*-use-internal-linkage)
929
930 void from_json(const nlohmann::json& j, SchedulePeriod& p) // NOLINT(*-use-internal-linkage)
931 {
932 j.at("start").get_to(p.start);
933 j.at("end").get_to(p.end);
934 }
935
936 void to_json(nlohmann::json& j, const Transmitter& t) // NOLINT(*-use-internal-linkage)
937 {
938 j = nlohmann::json{{"id", sim_id_to_json(t.getId())},
939 {"name", t.getName()},
940 {"waveform", sim_id_to_json((t.getSignal() != nullptr) ? t.getSignal()->getId() : 0)},
941 {"antenna", sim_id_to_json((t.getAntenna() != nullptr) ? t.getAntenna()->getId() : 0)},
942 {"timing", sim_id_to_json(t.getTiming() ? t.getTiming()->getId() : 0)}};
943
945 {
946 j["pulsed_mode"] = {{"prf", t.getPrf()}};
947 }
948 else if (t.getMode() == OperationMode::FMCW_MODE)
949 {
950 j["fmcw_mode"] = nlohmann::json::object();
951 }
952 else if (t.getMode() == OperationMode::SFCW_MODE)
953 {
954 j["sfcw_mode"] = nlohmann::json::object();
955 }
956 else
957 {
958 j["cw_mode"] = nlohmann::json::object();
959 }
960 if (!t.getSchedule().empty())
961 {
962 j["schedule"] = t.getSchedule();
963 }
964 }
965
966 void to_json(nlohmann::json& j, const Receiver& r) // NOLINT(*-use-internal-linkage)
967 {
968 j = nlohmann::json{{"id", sim_id_to_json(r.getId())},
969 {"name", r.getName()},
970 {"noise_temp", r.getNoiseTemperature()},
971 {"antenna", sim_id_to_json((r.getAntenna() != nullptr) ? r.getAntenna()->getId() : 0)},
972 {"timing", sim_id_to_json(r.getTiming() ? r.getTiming()->getId() : 0)},
973 {"nodirect", r.checkFlag(Receiver::RecvFlag::FLAG_NODIRECT)},
974 {"nopropagationloss", r.checkFlag(Receiver::RecvFlag::FLAG_NOPROPLOSS)}};
975
976 if (r.getMode() == OperationMode::PULSED_MODE)
977 {
978 j["pulsed_mode"] = {
979 {"prf", r.getWindowPrf()}, {"window_skip", r.getWindowSkip()}, {"window_length", r.getWindowLength()}};
980 }
981 else if (r.getMode() == OperationMode::FMCW_MODE)
982 {
983 j["fmcw_mode"] = receiver_fmcw_mode_to_json(r);
984 }
985 else if (r.getMode() == OperationMode::SFCW_MODE)
986 {
987 j["sfcw_mode"] = nlohmann::json::object();
988 }
989 else
990 {
991 j["cw_mode"] = nlohmann::json::object();
992 }
993 if (!r.getSchedule().empty())
994 {
995 j["schedule"] = r.getSchedule();
996 }
997 }
998
999 void to_json(nlohmann::json& j, const Target& t) // NOLINT(*-use-internal-linkage)
1000 {
1001 j["id"] = sim_id_to_json(t.getId());
1002 j["name"] = t.getName();
1003 nlohmann::json rcs_json;
1004 if (const auto* iso = dynamic_cast<const IsoTarget*>(&t))
1005 {
1006 rcs_json["type"] = "isotropic";
1007 rcs_json["value"] = iso->getConstRcs();
1008 }
1009 else if (const auto* file = dynamic_cast<const FileTarget*>(&t))
1010 {
1011 rcs_json["type"] = "file";
1012 rcs_json["filename"] = file->getFilename();
1013 }
1014 j["rcs"] = rcs_json;
1015
1016 // Serialize the fluctuation model if it exists.
1017 if (const auto* model_base = t.getFluctuationModel())
1018 {
1019 nlohmann::json model_json;
1020 if (const auto* chi_model = dynamic_cast<const RcsChiSquare*>(model_base))
1021 {
1022 model_json["type"] = "chisquare";
1023 model_json["k"] = chi_model->getK();
1024 }
1025 else // Default to constant if it's not a recognized type (e.g., RcsConst)
1026 {
1027 model_json["type"] = "constant";
1028 }
1029 j["model"] = model_json;
1030 }
1031 }
1032
1033 void to_json(nlohmann::json& j, const Platform& p) // NOLINT(*-use-internal-linkage)
1034 {
1035 j = {{"id", sim_id_to_json(p.getId())}, {"name", p.getName()}, {"motionpath", *p.getMotionPath()}};
1036
1037 if (p.getRotationPath()->getType() == math::RotationPath::InterpType::INTERP_CONSTANT)
1038 {
1039 j["fixedrotation"] = *p.getRotationPath();
1040 }
1041 else
1042 {
1043 j["rotationpath"] = *p.getRotationPath();
1044 }
1045 }
1046
1047}
1048
1049namespace params
1050{
1052 {{CoordinateFrame::ENU, "ENU"},
1053 {CoordinateFrame::UTM, "UTM"},
1054 {CoordinateFrame::ECEF, "ECEF"}})
1057
1058 void to_json(nlohmann::json& j, const Parameters& p) // NOLINT(*-use-internal-linkage)
1059 {
1060 j = nlohmann::json{{"starttime", p.start},
1061 {"endtime", p.end},
1062 {"rate", p.rate},
1063 {"c", p.c},
1064 {"simSamplingRate", p.sim_sampling_rate},
1065 {"adc_bits", p.adc_bits},
1066 {"oversample", p.oversample_ratio},
1067 {"rotationangleunit", p.rotation_angle_unit}};
1068
1069 if (p.random_seed.has_value())
1070 {
1071 j["randomseed"] = p.random_seed.value();
1072 }
1073
1074 j["origin"] = {
1075 {"latitude", p.origin_latitude}, {"longitude", p.origin_longitude}, {"altitude", p.origin_altitude}};
1076
1077 j["coordinatesystem"] = {{"frame", p.coordinate_frame}};
1078 if (p.coordinate_frame == CoordinateFrame::UTM)
1079 {
1080 j["coordinatesystem"]["zone"] = p.utm_zone;
1081 j["coordinatesystem"]["hemisphere"] = p.utm_north_hemisphere ? "N" : "S";
1082 }
1083 }
1084
1085 void from_json(const nlohmann::json& j, Parameters& p) // NOLINT(*-use-internal-linkage)
1086 {
1087 p.start = j.at("starttime").get<RealType>();
1088 p.end = j.at("endtime").get<RealType>();
1089 p.rate = j.at("rate").get<RealType>();
1090 p.c = j.value("c", Parameters::DEFAULT_C);
1091 p.sim_sampling_rate = j.value("simSamplingRate", 1000.0);
1092 p.adc_bits = j.value("adc_bits", 0u);
1093 p.oversample_ratio = j.value("oversample", 1u);
1094 params::validateOversampleRatio(p.oversample_ratio);
1095 p.rotation_angle_unit = j.value("rotationangleunit", RotationAngleUnit::Degrees);
1096 p.random_seed = j.value<std::optional<unsigned>>("randomseed", std::nullopt);
1097
1098 const auto& origin = j.at("origin");
1099 p.origin_latitude = origin.at("latitude").get<double>();
1100 p.origin_longitude = origin.at("longitude").get<double>();
1101 p.origin_altitude = origin.at("altitude").get<double>();
1102
1103 const auto& cs = j.at("coordinatesystem");
1104 p.coordinate_frame = cs.at("frame").get<CoordinateFrame>();
1105 if (p.coordinate_frame == CoordinateFrame::UTM)
1106 {
1107 p.utm_zone = cs.at("zone").get<int>();
1108 p.utm_north_hemisphere = cs.at("hemisphere").get<std::string>() == "N";
1109 }
1110 }
1111}
1112
1113namespace
1114{
1117 {
1118 monostatic_comp["noise_temp"] = receiver.getNoiseTemperature();
1121
1122 if (!transmitter.getSchedule().empty())
1123 {
1124 monostatic_comp["schedule"] = transmitter.getSchedule();
1125 }
1126
1128 {
1129 monostatic_comp["pulsed_mode"] = {{"prf", transmitter.getPrf()},
1130 {"window_skip", receiver.getWindowSkip()},
1131 {"window_length", receiver.getWindowLength()}};
1132 }
1133 else if (transmitter.getMode() == radar::OperationMode::FMCW_MODE)
1134 {
1136 }
1137 else if (transmitter.getMode() == radar::OperationMode::SFCW_MODE)
1138 {
1139 monostatic_comp["sfcw_mode"] = nlohmann::json::object();
1140 }
1141 else
1142 {
1143 monostatic_comp["cw_mode"] = nlohmann::json::object();
1144 }
1145 }
1146
1148 {
1149 const auto* attached = transmitter.getAttached();
1150 nlohmann::json monostatic_comp;
1151 monostatic_comp["name"] = transmitter.getName();
1152 monostatic_comp["tx_id"] = sim_id_to_json(transmitter.getId());
1153 monostatic_comp["rx_id"] = sim_id_to_json(attached->getId());
1154 monostatic_comp["waveform"] =
1155 sim_id_to_json((transmitter.getSignal() != nullptr) ? transmitter.getSignal()->getId() : 0);
1156 monostatic_comp["antenna"] =
1157 sim_id_to_json((transmitter.getAntenna() != nullptr) ? transmitter.getAntenna()->getId() : 0);
1158 monostatic_comp["timing"] = sim_id_to_json(transmitter.getTiming() ? transmitter.getTiming()->getId() : 0);
1159
1160 if (const auto* receiver = dynamic_cast<const radar::Receiver*>(attached))
1161 {
1163 }
1164 return nlohmann::json{{"monostatic", monostatic_comp}};
1165 }
1166
1168 const core::World& world)
1169 {
1170 for (const auto& transmitter : world.getTransmitters())
1171 {
1172 if (transmitter->getPlatform() != platform)
1173 {
1174 continue;
1175 }
1176 if (transmitter->getAttached() != nullptr)
1177 {
1179 }
1180 else
1181 {
1182 components.push_back(nlohmann::json{{"transmitter", *transmitter}});
1183 }
1184 }
1185 }
1186
1187 void appendReceiverComponents(nlohmann::json& components, const radar::Platform* platform, const core::World& world)
1188 {
1189 for (const auto& receiver : world.getReceivers())
1190 {
1191 if (receiver->getPlatform() == platform && receiver->getAttached() == nullptr)
1192 {
1193 components.push_back(nlohmann::json{{"receiver", *receiver}});
1194 }
1195 }
1196 }
1197
1198 void appendTargetComponents(nlohmann::json& components, const radar::Platform* platform, const core::World& world)
1199 {
1200 for (const auto& target : world.getTargets())
1201 {
1202 if (target->getPlatform() == platform)
1203 {
1204 components.push_back(nlohmann::json{{"target", *target}});
1205 }
1206 }
1207 }
1208
1209 /// Serializes a platform and its attached components to JSON.
1210 nlohmann::json serialize_platform(const radar::Platform* p, const core::World& world)
1211 {
1212 nlohmann::json plat_json = *p;
1213 plat_json["components"] = nlohmann::json::array();
1214 auto& components = plat_json["components"];
1215
1219
1220 return plat_json;
1221 }
1222
1223 /// Parses simulation parameters from JSON and updates the master seeder.
1224 void parse_parameters(const nlohmann::json& sim, std::mt19937& masterSeeder)
1225 {
1226 auto new_params = sim.at("parameters").get<params::Parameters>();
1227
1228 // If a random seed is present in the incoming JSON, it is used to re-seed
1229 // the master generator. This is crucial for allowing the UI to control
1230 // simulation reproducibility.
1231 if (sim.at("parameters").contains("randomseed"))
1232 {
1234 if (params::params.random_seed)
1235 {
1236 LOG(logging::Level::INFO, "Master seed updated from JSON to: {}", *params::params.random_seed);
1237 masterSeeder.seed(*params::params.random_seed);
1238 }
1239 }
1240
1243 params::params.simulation_name = sim.value("name", "");
1244 }
1245
1246 /// Parses top-level reusable assets from JSON into the world.
1247 void parse_assets(const nlohmann::json& sim, core::World& world)
1248 {
1249 if (sim.contains("waveforms"))
1250 {
1251 for (auto waveforms = sim.at("waveforms").get<std::vector<std::unique_ptr<fers_signal::RadarSignal>>>();
1252 auto& waveform : waveforms)
1253 {
1254 // Only add valid waveforms. If filename was empty, waveform is nullptr.
1255 if (waveform)
1256 {
1257 world.add(std::move(waveform));
1258 }
1259 }
1260 }
1261
1262 if (sim.contains("antennas"))
1263 {
1264 for (auto antennas = sim.at("antennas").get<std::vector<std::unique_ptr<antenna::Antenna>>>();
1265 auto& antenna : antennas)
1266 {
1267 // Only add valid antennas.
1268 if (antenna)
1269 {
1270 world.add(std::move(antenna));
1271 }
1272 }
1273 }
1274
1275 if (sim.contains("timings"))
1276 {
1277 for (const auto& timing_json : sim.at("timings"))
1278 {
1279 auto name = timing_json.at("name").get<std::string>();
1280 const auto timing_id = parse_json_id(timing_json, "id", "Timing");
1281 auto timing_obj = std::make_unique<timing::PrototypeTiming>(name, timing_id);
1282 timing_json.get_to(*timing_obj);
1283 world.add(std::move(timing_obj));
1284 }
1285 }
1286 }
1287
1288 using JsonNameRegistry = std::unordered_map<std::string, std::string>;
1289
1290 void register_json_name(JsonNameRegistry& name_registry, const nlohmann::json& element, const std::string_view kind)
1291 {
1292 if (!element.is_object() || !element.contains("name"))
1293 {
1294 return;
1295 }
1296
1297 const auto name = element.at("name").get<std::string>();
1298 const auto [iter, inserted] = name_registry.emplace(name, std::string(kind));
1299 if (!inserted)
1300 {
1301 throw std::runtime_error("Duplicate name '" + name + "' found for " + std::string(kind) +
1302 "; previously used by " + iter->second + ".");
1303 }
1304 }
1305
1306 void register_json_name_array(JsonNameRegistry& name_registry, const nlohmann::json& sim,
1307 const std::string_view key, const std::string_view kind)
1308 {
1309 const std::string key_string(key);
1310 if (!sim.contains(key_string))
1311 {
1312 return;
1313 }
1314 for (const auto& element : sim.at(key_string))
1315 {
1317 }
1318 }
1319
1321 {
1322 if (!platform.contains("components") || !platform.at("components").is_array())
1323 {
1324 return;
1325 }
1326
1327 for (const auto& component_wrapper : platform.at("components"))
1328 {
1329 if (!component_wrapper.is_object())
1330 {
1331 continue;
1332 }
1333 for (const auto& [kind, component] : component_wrapper.items())
1334 {
1336 }
1337 }
1338 }
1339
1340 void register_platform_names(JsonNameRegistry& name_registry, const nlohmann::json& sim)
1341 {
1342 if (!sim.contains("platforms"))
1343 {
1344 return;
1345 }
1346
1347 for (const auto& platform : sim.at("platforms"))
1348 {
1351 }
1352 }
1353
1354 void validate_unique_names(const nlohmann::json& sim)
1355 {
1357 name_registry.reserve(64);
1358 register_json_name_array(name_registry, sim, "waveforms", "waveform");
1359 register_json_name_array(name_registry, sim, "timings", "timing");
1360 register_json_name_array(name_registry, sim, "antennas", "antenna");
1362 }
1363
1364 /// Counts operation mode blocks on a component.
1365 std::size_t mode_block_count(const nlohmann::json& comp_json)
1366 {
1367 return static_cast<std::size_t>(comp_json.contains("pulsed_mode")) +
1368 static_cast<std::size_t>(comp_json.contains("fmcw_mode")) +
1369 static_cast<std::size_t>(comp_json.contains("cw_mode")) +
1370 static_cast<std::size_t>(comp_json.contains("sfcw_mode"));
1371 }
1372
1373 /// Throws when a partial update or full component declares conflicting modes.
1374 void reject_conflicting_mode_blocks(const nlohmann::json& comp_json, const std::string& error_context)
1375 {
1376 if (mode_block_count(comp_json) > 1)
1377 {
1378 throw std::runtime_error(error_context +
1379 " must have at most one of 'pulsed_mode', 'cw_mode', "
1380 "'fmcw_mode', or 'sfcw_mode'.");
1381 }
1382 }
1383
1384 /// Parses the mutually exclusive operation mode block for a component.
1385 radar::OperationMode parse_mode(const nlohmann::json& comp_json, const std::string& error_context)
1386 {
1388 if (comp_json.contains("pulsed_mode"))
1389 {
1391 }
1392 if (comp_json.contains("fmcw_mode"))
1393 {
1395 }
1396 if (comp_json.contains("sfcw_mode"))
1397 {
1399 }
1400 if (comp_json.contains("cw_mode"))
1401 {
1403 }
1404 throw std::runtime_error(error_context +
1405 " must have a 'pulsed_mode', 'cw_mode', or 'fmcw_mode' block, or an 'sfcw_mode' "
1406 "block.");
1407 }
1408
1409 /// Parses a transmitter component from JSON into the world.
1410 void parse_transmitter(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1411 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1412 {
1413 // --- Dependency Check ---
1414 // Validate Waveform and Timing existence before creation to prevent core crashes.
1415 const auto wave_id = parse_json_id(comp_json, "waveform", "Transmitter");
1416 const auto timing_id = parse_json_id(comp_json, "timing", "Transmitter");
1417 const auto antenna_id = parse_json_id(comp_json, "antenna", "Transmitter");
1418
1419 if (world.findWaveform(wave_id) == nullptr)
1420 {
1421 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid waveform '{}'.",
1422 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "waveform"));
1423 return;
1424 }
1425 if (world.findTiming(timing_id) == nullptr)
1426 {
1427 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid timing source '{}'.",
1428 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1429 return;
1430 }
1431 if (world.findAntenna(antenna_id) == nullptr)
1432 {
1433 LOG(logging::Level::WARNING, "Skipping Transmitter '{}': Missing or invalid antenna '{}'.",
1434 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1435 return;
1436 }
1437
1438 radar::OperationMode const mode =
1439 parse_mode(comp_json, "Transmitter component '" + comp_json.value("name", "Unnamed") + "'");
1440 reject_non_empty_sfcw_mode(comp_json, "Transmitter component '" + comp_json.value("name", "Unnamed") + "'");
1441 if (mode == radar::OperationMode::FMCW_MODE && comp_json.contains("fmcw_mode") &&
1442 has_dechirp_fields(comp_json.at("fmcw_mode")))
1443 {
1444 throw std::runtime_error("Transmitter component '" + comp_json.value("name", "Unnamed") +
1445 "' fmcw_mode must not contain dechirp configuration.");
1446 }
1447
1448 const auto trans_id = parse_json_id(comp_json, "id", "Transmitter");
1449 auto trans = std::make_unique<radar::Transmitter>(plat, comp_json.value("name", "Unnamed"), mode, trans_id);
1450 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1451 {
1452 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
1453 }
1454
1455 auto* const waveform = world.findWaveform(wave_id);
1456 validate_fmcw_waveform(*waveform, "Waveform '" + waveform->getName() + "'");
1457 validate_waveform_mode_match(*waveform, mode,
1458 "Transmitter component '" + comp_json.value("name", "Unnamed") + "'");
1459 trans->setWave(waveform);
1460 trans->setAntenna(world.findAntenna(antenna_id));
1461
1462 if (const auto timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1463 {
1464 trans->setTiming(timing);
1465 }
1466
1467 if (comp_json.contains("schedule"))
1468 {
1469 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1470 RealType pri = 0.0;
1472 {
1473 pri = 1.0 / trans->getPrf();
1474 }
1475 auto schedule =
1477 if (waveform->isFmcwFamily() || waveform->isSteppedFrequency())
1478 {
1479 validate_fmcw_schedule(schedule, *waveform, "Transmitter component '" + trans->getName() + "'");
1480 }
1481 trans->setSchedule(std::move(schedule));
1482 }
1483 else if (waveform->isFmcwFamily() || waveform->isSteppedFrequency())
1484 {
1485 validate_fmcw_schedule(trans->getSchedule(), *waveform, "Transmitter component '" + trans->getName() + "'");
1486 }
1487
1488 world.add(std::move(trans));
1489 }
1490
1491 /// Parses a receiver component from JSON into the world.
1492 void parse_receiver(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1493 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1494 {
1495 // --- Dependency Check ---
1496 // Receiver strictly requires a Timing source.
1497 const auto timing_id = parse_json_id(comp_json, "timing", "Receiver");
1498 const auto antenna_id = parse_json_id(comp_json, "antenna", "Receiver");
1499
1500 if (world.findTiming(timing_id) == nullptr)
1501 {
1502 LOG(logging::Level::WARNING, "Skipping Receiver '{}': Missing or invalid timing source '{}'.",
1503 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1504 return;
1505 }
1506
1507 if (world.findAntenna(antenna_id) == nullptr)
1508 {
1509 LOG(logging::Level::WARNING, "Skipping Receiver '{}': Missing or invalid antenna '{}'.",
1510 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1511 return;
1512 }
1513
1514 radar::OperationMode const mode =
1515 parse_mode(comp_json, "Receiver component '" + comp_json.value("name", "Unnamed") + "'");
1516 reject_non_empty_sfcw_mode(comp_json, "Receiver component '" + comp_json.value("name", "Unnamed") + "'");
1517
1518 const auto recv_id = parse_json_id(comp_json, "id", "Receiver");
1519 auto recv =
1520 std::make_unique<radar::Receiver>(plat, comp_json.value("name", "Unnamed"), masterSeeder(), mode, recv_id);
1521 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1522 {
1523 const auto& mode_json = comp_json.at("pulsed_mode");
1524 recv->setWindowProperties(mode_json.value("window_length", 0.0), mode_json.value("prf", 0.0),
1525 mode_json.value("window_skip", 0.0));
1526 }
1527
1528 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
1529
1530 recv->setAntenna(world.findAntenna(antenna_id));
1531
1532 if (const auto timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1533 {
1534 recv->setTiming(timing);
1535 }
1536
1537 if (comp_json.value("nodirect", false))
1538 {
1540 }
1541 if (comp_json.value("nopropagationloss", false))
1542 {
1544 }
1545
1546 if (comp_json.contains("schedule"))
1547 {
1548 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1549 RealType pri = 0.0;
1551 {
1552 pri = 1.0 / recv->getWindowPrf();
1553 }
1554 recv->setSchedule(
1556 }
1557
1559 "Receiver component '" + comp_json.value("name", "Unnamed") + "'");
1560 world.add(std::move(recv));
1561 }
1562
1563 /// Parses a target component from JSON into the world.
1564 void parse_target(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1565 std::mt19937& masterSeeder)
1566 {
1567 const auto& rcs_json = comp_json.at("rcs");
1568 const auto rcs_type = rcs_json.at("type").get<std::string>();
1569 std::unique_ptr<radar::Target> target_obj;
1570
1571 if (rcs_type == "isotropic")
1572 {
1573 const auto target_id = parse_json_id(comp_json, "id", "Target");
1574 target_obj = radar::createIsoTarget(plat, comp_json.at("name").get<std::string>(),
1575 rcs_json.at("value").get<RealType>(),
1576 static_cast<unsigned>(masterSeeder()), target_id);
1577 }
1578 else if (rcs_type == "file")
1579 {
1580 const auto filename = rcs_json.value("filename", "");
1581 if (filename.empty())
1582 {
1583 LOG(logging::Level::WARNING, "Skipping load of file target '{}': RCS filename is empty.",
1584 comp_json.value("name", "Unknown"));
1585 return;
1586 }
1587 const auto target_id = parse_json_id(comp_json, "id", "Target");
1588 target_obj = radar::createFileTarget(plat, comp_json.at("name").get<std::string>(), filename,
1589 static_cast<unsigned>(masterSeeder()), target_id);
1590 }
1591 else
1592 {
1593 throw std::runtime_error("Unsupported target RCS type: " + rcs_type);
1594 }
1595 world.add(std::move(target_obj));
1596
1597 // After creating the target, check for and apply the fluctuation model.
1598 if (comp_json.contains("model"))
1599 {
1600 const auto& model_json = comp_json.at("model");
1601 const auto model_type = model_json.at("type").get<std::string>();
1602
1603 if (model_type == "chisquare" || model_type == "gamma")
1604 {
1605 auto model = std::make_unique<radar::RcsChiSquare>(world.getTargets().back()->getRngEngine(),
1606 model_json.at("k").get<RealType>());
1607 world.getTargets().back()->setFluctuationModel(std::move(model));
1608 }
1609 else if (model_type == "constant")
1610 {
1611 world.getTargets().back()->setFluctuationModel(std::make_unique<radar::RcsConst>());
1612 }
1613 else
1614 {
1615 throw std::runtime_error("Unsupported fluctuation model type: " + model_type);
1616 }
1617 }
1618 }
1619
1620 /// Parses a monostatic component into linked transmitter and receiver objects.
1621 void parse_monostatic(const nlohmann::json& comp_json, radar::Platform* plat, core::World& world,
1622 std::mt19937& masterSeeder, TimingInstanceMap& timing_instances)
1623 {
1624 // This block reconstructs the internal C++ representation of a
1625 // monostatic radar (a linked Transmitter and Receiver) from the
1626 // single 'monostatic' component in the JSON.
1627 // --- Dependency Check ---
1628 const auto wave_id = parse_json_id(comp_json, "waveform", "Monostatic");
1629 const auto timing_id = parse_json_id(comp_json, "timing", "Monostatic");
1630 const auto antenna_id = parse_json_id(comp_json, "antenna", "Monostatic");
1631
1632 if (world.findWaveform(wave_id) == nullptr)
1633 {
1634 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid waveform '{}'.",
1635 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "waveform"));
1636 return;
1637 }
1638 if (world.findTiming(timing_id) == nullptr)
1639 {
1640 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid timing source '{}'.",
1641 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "timing"));
1642 return;
1643 }
1644 if (world.findAntenna(antenna_id) == nullptr)
1645 {
1646 LOG(logging::Level::WARNING, "Skipping Monostatic '{}': Missing or invalid antenna '{}'.",
1647 comp_json.value("name", "Unnamed"), json_field_for_log(comp_json, "antenna"));
1648 return;
1649 }
1650
1651 radar::OperationMode const mode =
1652 parse_mode(comp_json, "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1653 reject_non_empty_sfcw_mode(comp_json, "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1654
1655 // Transmitter part
1656 const auto tx_id = parse_json_id(comp_json, "tx_id", "Monostatic");
1657 auto trans = std::make_unique<radar::Transmitter>(plat, comp_json.value("name", "Unnamed"), mode, tx_id);
1658 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1659 {
1660 trans->setPrf(comp_json.at("pulsed_mode").value("prf", 0.0));
1661 }
1662
1663 auto* const waveform = world.findWaveform(wave_id);
1664 validate_fmcw_waveform(*waveform, "Waveform '" + waveform->getName() + "'");
1665 validate_waveform_mode_match(*waveform, mode,
1666 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1667 trans->setWave(waveform);
1668 trans->setAntenna(world.findAntenna(antenna_id));
1669 if (const auto shared_timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1670 {
1671 trans->setTiming(shared_timing);
1672 }
1673
1674 // Receiver part
1675 const auto rx_id = parse_json_id(comp_json, "rx_id", "Monostatic");
1676 auto recv =
1677 std::make_unique<radar::Receiver>(plat, comp_json.value("name", "Unnamed"), masterSeeder(), mode, rx_id);
1678 if (mode == radar::OperationMode::PULSED_MODE && comp_json.contains("pulsed_mode"))
1679 {
1680 const auto& mode_json = comp_json.at("pulsed_mode");
1681 recv->setWindowProperties(mode_json.value("window_length", 0.0),
1682 trans->getPrf(), // Use transmitter's PRF
1683 mode_json.value("window_skip", 0.0));
1684 }
1685 recv->setNoiseTemperature(comp_json.value("noise_temp", 0.0));
1686
1687 recv->setAntenna(world.findAntenna(antenna_id));
1688 if (const auto shared_timing = resolve_timing_instance(world, masterSeeder, timing_instances, timing_id))
1689 {
1690 recv->setTiming(shared_timing);
1691 }
1692
1693 if (comp_json.value("nodirect", false))
1694 {
1696 }
1697 if (comp_json.value("nopropagationloss", false))
1698 {
1700 }
1701 if (comp_json.contains("schedule"))
1702 {
1703 auto raw = comp_json.at("schedule").get<std::vector<radar::SchedulePeriod>>();
1704 RealType pri = 0.0;
1706 {
1707 pri = 1.0 / trans->getPrf();
1708 }
1709
1710 // Process once, apply to both
1711 auto processed_schedule =
1713 if (waveform->isFmcwFamily() || waveform->isSteppedFrequency())
1714 {
1716 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1717 }
1718
1719 trans->setSchedule(processed_schedule);
1720 recv->setSchedule(processed_schedule);
1721 }
1722 else if (waveform->isFmcwFamily() || waveform->isSteppedFrequency())
1723 {
1724 validate_fmcw_schedule(trans->getSchedule(), *waveform,
1725 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1726 }
1727
1728 // Link them and add to world
1729 trans->setAttached(recv.get());
1730 recv->setAttached(trans.get());
1732 "Monostatic component '" + comp_json.value("name", "Unnamed") + "'");
1733 world.add(std::move(trans));
1734 world.add(std::move(recv));
1735 }
1736
1737 /// Parses a platform and its component list from JSON into the world.
1738 void parse_platform(const nlohmann::json& plat_json, core::World& world, std::mt19937& masterSeeder,
1739 TimingInstanceMap& timing_instances)
1740 {
1741 auto name = plat_json.at("name").get<std::string>();
1742 const auto platform_id = parse_json_id(plat_json, "id", "Platform");
1743 auto plat = std::make_unique<radar::Platform>(name, platform_id);
1744
1746
1747 // Components - Strict array format
1748 if (plat_json.contains("components"))
1749 {
1750 for (const auto& comp_json_outer : plat_json.at("components"))
1751 {
1752 if (comp_json_outer.contains("transmitter"))
1753 {
1754 parse_transmitter(comp_json_outer.at("transmitter"), plat.get(), world, masterSeeder,
1755 timing_instances);
1756 }
1757 else if (comp_json_outer.contains("receiver"))
1758 {
1759 parse_receiver(comp_json_outer.at("receiver"), plat.get(), world, masterSeeder, timing_instances);
1760 }
1761 else if (comp_json_outer.contains("target"))
1762 {
1763 parse_target(comp_json_outer.at("target"), plat.get(), world, masterSeeder);
1764 }
1765 else if (comp_json_outer.contains("monostatic"))
1766 {
1767 parse_monostatic(comp_json_outer.at("monostatic"), plat.get(), world, masterSeeder,
1768 timing_instances);
1769 }
1770 }
1771 }
1772
1773 world.add(std::move(plat));
1774 }
1775
1776 void populate_world_from_json(const nlohmann::json& j, core::World& world, std::mt19937& masterSeeder)
1777 {
1778 world.clear();
1779
1780 const auto& sim = j.at("simulation");
1782
1784 parse_assets(sim, world);
1785
1786 if (sim.contains("platforms"))
1787 {
1788 TimingInstanceMap timing_instances;
1789 for (const auto& plat_json : sim.at("platforms"))
1790 {
1791 parse_platform(plat_json, world, masterSeeder, timing_instances);
1792 }
1793 }
1795
1796 world.scheduleInitialEvents();
1797 }
1798}
1799
1800namespace serial
1801{
1802 std::unique_ptr<antenna::Antenna> parse_antenna_from_json(const nlohmann::json& j)
1803 {
1804 std::unique_ptr<antenna::Antenna> ant;
1806 return ant;
1807 }
1808
1809 std::unique_ptr<fers_signal::RadarSignal> parse_waveform_from_json(const nlohmann::json& j)
1810 {
1811 std::unique_ptr<fers_signal::RadarSignal> wf;
1813 return wf;
1814 }
1815
1816 std::unique_ptr<timing::PrototypeTiming> parse_timing_from_json(const nlohmann::json& j, const SimId id)
1817 {
1818 auto timing = std::make_unique<timing::PrototypeTiming>(j.at("name").get<std::string>(), id);
1819 j.get_to(*timing);
1820 return timing;
1821 }
1822
1823 void update_parameters_from_json(const nlohmann::json& j, std::mt19937& masterSeeder)
1824 {
1825 nlohmann::json sim;
1826 sim["parameters"] = j;
1828 }
1829
1830 std::unique_ptr<antenna::Antenna> parse_required_update_antenna(const nlohmann::json& j)
1831 {
1833 if (parsed == nullptr)
1834 {
1835 const auto name = j.value("name", std::string{});
1836 const auto pattern = j.value("pattern", "isotropic");
1837 throw std::runtime_error("Cannot update antenna '" + name + "' to pattern '" + pattern +
1838 "' without a filename.");
1839 }
1840 return parsed;
1841 }
1842
1843 bool antenna_pattern_requires_replacement(const std::string_view pattern, const antenna::Antenna* ant) noexcept
1844 {
1845 if (pattern == "isotropic")
1846 {
1847 return dynamic_cast<const antenna::Isotropic*>(ant) == nullptr;
1848 }
1849 if (pattern == "sinc")
1850 {
1851 return dynamic_cast<const antenna::Sinc*>(ant) == nullptr;
1852 }
1853 if (pattern == "gaussian")
1854 {
1855 return dynamic_cast<const antenna::Gaussian*>(ant) == nullptr;
1856 }
1857 if (pattern == "squarehorn")
1858 {
1859 return dynamic_cast<const antenna::SquareHorn*>(ant) == nullptr;
1860 }
1861 if (pattern == "parabolic")
1862 {
1863 return dynamic_cast<const antenna::Parabolic*>(ant) == nullptr;
1864 }
1865 if (pattern == "xml")
1866 {
1867 return dynamic_cast<const antenna::XmlAntenna*>(ant) == nullptr;
1868 }
1869 if (pattern == "file")
1870 {
1871 return dynamic_cast<const antenna::H5Antenna*>(ant) == nullptr;
1872 }
1873 return false;
1874 }
1875
1877 {
1878 if (auto* sinc = dynamic_cast<antenna::Sinc*>(ant))
1879 {
1880 sinc->setAlpha(j.value("alpha", 1.0));
1881 sinc->setBeta(j.value("beta", 1.0));
1882 sinc->setGamma(j.value("gamma", 2.0));
1883 }
1884 else if (auto* gauss = dynamic_cast<antenna::Gaussian*>(ant))
1885 {
1886 gauss->setAzimuthScale(j.value("azscale", 1.0));
1887 gauss->setElevationScale(j.value("elscale", 1.0));
1888 }
1889 else if (auto* horn = dynamic_cast<antenna::SquareHorn*>(ant))
1890 {
1891 horn->setDimension(j.value("diameter", 0.5));
1892 }
1893 else if (auto* para = dynamic_cast<antenna::Parabolic*>(ant))
1894 {
1895 para->setDiameter(j.value("diameter", 0.5));
1896 }
1897 else if (auto* xml = dynamic_cast<antenna::XmlAntenna*>(ant))
1898 {
1899 if (xml->getFilename() != j.value("filename", ""))
1900 {
1902 }
1903 }
1904 else if (auto* h5 = dynamic_cast<antenna::H5Antenna*>(ant))
1905 {
1906 if (h5->getFilename() != j.value("filename", ""))
1907 {
1909 }
1910 }
1911 }
1912
1913 void update_antenna_from_json(const nlohmann::json& j, antenna::Antenna* ant, core::World& world)
1914 {
1915 const auto new_pattern = j.value("pattern", "isotropic");
1917 {
1919 return;
1920 }
1921
1922 ant->setName(j.at("name").get<std::string>());
1923 ant->setEfficiencyFactor(j.value("efficiency", 1.0));
1925 }
1926
1928 {
1929 if (j.contains("motionpath"))
1930 {
1931 auto path = std::make_unique<math::Path>();
1932 j.at("motionpath").get_to(*path);
1933 plat->setMotionPath(std::move(path));
1934 }
1935 if (j.contains("rotationpath"))
1936 {
1937 auto rot_path = std::make_unique<math::RotationPath>();
1938 const auto& rotation_json = j.at("rotationpath");
1939 rot_path->setInterp(rotation_json.at("interpolation").get<math::RotationPath::InterpType>());
1940 unsigned waypoint_index = 0;
1941 for (const auto& waypoint_json : rotation_json.at("rotationwaypoints"))
1942 {
1943 const RealType azimuth = waypoint_json.at("azimuth").get<RealType>();
1944 const RealType elevation = waypoint_json.at("elevation").get<RealType>();
1945 const RealType time = waypoint_json.at("time").get<RealType>();
1946 const std::string owner =
1947 std::format("platform '{}' rotation waypoint {}", plat->getName(), waypoint_index);
1948
1951 "JSON", owner, "azimuth");
1954 "JSON", owner, "elevation");
1955
1956 rot_path->addCoord(rotation_angle_utils::external_rotation_to_internal(azimuth, elevation, time,
1959 }
1960 rot_path->finalize();
1961 plat->setRotationPath(std::move(rot_path));
1962 }
1963 else if (j.contains("fixedrotation"))
1964 {
1965 auto rot_path = std::make_unique<math::RotationPath>();
1966 const auto& fixed_json = j.at("fixedrotation");
1967 const RealType start_az_deg = fixed_json.at("startazimuth").get<RealType>();
1968 const RealType start_el_deg = fixed_json.at("startelevation").get<RealType>();
1969 const RealType rate_az_deg_s = fixed_json.at("azimuthrate").get<RealType>();
1970 const RealType rate_el_deg_s = fixed_json.at("elevationrate").get<RealType>();
1971 const std::string owner = std::format("platform '{}' fixedrotation", plat->getName());
1972
1975 owner, "startazimuth");
1978 owner, "startelevation");
1981 owner, "azimuthrate");
1984 owner, "elevationrate");
1985
1990 rot_path->setConstantRate(start, rate);
1991 rot_path->finalize();
1992 plat->setRotationPath(std::move(rot_path));
1993 }
1994 }
1995
1997 {
1998 reject_conflicting_mode_blocks(j, "Transmitter '" + tx.getName() + "'");
1999 if (j.contains("pulsed_mode"))
2000 {
2002 tx.setPrf(j.at("pulsed_mode").value("prf", 0.0));
2003 }
2004 else if (j.contains("fmcw_mode"))
2005 {
2006 if (has_dechirp_fields(j.at("fmcw_mode")))
2007 {
2008 throw std::runtime_error("Transmitter '" + tx.getName() +
2009 "' fmcw_mode must not contain dechirp configuration.");
2010 }
2012 }
2013 else if (j.contains("sfcw_mode"))
2014 {
2015 reject_non_empty_sfcw_mode(j, "Transmitter '" + tx.getName() + "'");
2017 }
2018 else if (j.contains("cw_mode"))
2019 {
2021 }
2022 }
2023
2025 {
2026 if (!j.contains("waveform"))
2027 {
2028 return;
2029 }
2030 auto id = parse_json_id(j, "waveform", "Transmitter");
2031 auto* wf = world.findWaveform(id);
2032 if (wf == nullptr)
2033 {
2034 throw std::runtime_error("Waveform ID " + std::to_string(id) + " not found.");
2035 }
2036 validate_fmcw_waveform(*wf, "Waveform '" + wf->getName() + "'");
2037 validate_waveform_mode_match(*wf, tx.getMode(), "Transmitter '" + tx.getName() + "'");
2038 tx.setWave(wf);
2039 }
2040
2042 {
2043 if (!j.contains("antenna"))
2044 {
2045 return;
2046 }
2047 auto id = parse_json_id(j, "antenna", "Transmitter");
2048 auto* ant = world.findAntenna(id);
2049 if (ant == nullptr)
2050 {
2051 throw std::runtime_error("Antenna ID " + std::to_string(id) + " not found.");
2052 }
2053 tx.setAntenna(ant);
2054 }
2055
2057 {
2058 if (!j.contains("timing"))
2059 {
2060 return;
2061 }
2062 auto timing_id = parse_json_id(j, "timing", "Transmitter");
2063 auto* const timing_proto = world.findTiming(timing_id);
2064 if (timing_proto == nullptr)
2065 {
2066 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
2067 }
2068 unsigned const seed = tx.getTiming() ? tx.getTiming()->getSeed() : 0;
2069 auto timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
2070 timing->initializeModel(timing_proto);
2071 tx.setTiming(timing);
2072 }
2073
2075 {
2076 if (tx.getSignal() == nullptr)
2077 {
2078 return;
2079 }
2080 validate_fmcw_waveform(*tx.getSignal(), "Waveform '" + tx.getSignal()->getName() + "'");
2081 validate_waveform_mode_match(*tx.getSignal(), tx.getMode(), owner);
2082 if (tx.getSignal()->isFmcwFamily() || tx.getSignal()->isSteppedFrequency())
2083 {
2084 validate_fmcw_schedule(tx.getSchedule(), *tx.getSignal(), owner);
2085 }
2086 }
2087
2089 const std::string& owner)
2090 {
2091 if (!j.contains("schedule"))
2092 {
2093 return;
2094 }
2095 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
2096 const bool pulsed = tx.getMode() == radar::OperationMode::PULSED_MODE;
2097 const RealType pri = pulsed ? 1.0 / tx.getPrf() : 0.0;
2098 auto schedule = radar::processRawSchedule(raw, tx.getName(), pulsed, pri);
2099 if (tx.getSignal() != nullptr && (tx.getSignal()->isFmcwFamily() || tx.getSignal()->isSteppedFrequency()))
2100 {
2101 validate_fmcw_schedule(schedule, *tx.getSignal(), owner);
2102 }
2103 tx.setSchedule(std::move(schedule));
2104 }
2105
2107 std::mt19937& /*masterSeeder*/)
2108 {
2109 if (j.contains("name"))
2110 tx->setName(j.at("name").get<std::string>());
2111
2112 const std::string owner = "Transmitter '" + tx->getName() + "'";
2119 }
2120
2122 {
2123 reject_conflicting_mode_blocks(j, "Receiver '" + rx.getName() + "'");
2124 if (j.contains("pulsed_mode"))
2125 {
2127 const auto& mode_json = j.at("pulsed_mode");
2128 rx.setWindowProperties(mode_json.value("window_length", 0.0), mode_json.value("prf", 0.0),
2129 mode_json.value("window_skip", 0.0));
2130 }
2131 else if (j.contains("fmcw_mode"))
2132 {
2134 }
2135 else if (j.contains("sfcw_mode"))
2136 {
2137 reject_non_empty_sfcw_mode(j, "Receiver '" + rx.getName() + "'");
2139 }
2140 else if (j.contains("cw_mode"))
2141 {
2143 }
2144 }
2145
2147 {
2148 if (j.contains("noise_temp"))
2149 rx.setNoiseTemperature(j.value("noise_temp", 0.0));
2150
2151 if (j.contains("nodirect"))
2152 {
2153 if (j.value("nodirect", false))
2155 else
2157 }
2158 if (j.contains("nopropagationloss"))
2159 {
2160 if (j.value("nopropagationloss", false))
2162 else
2164 }
2165 }
2166
2168 {
2169 if (!j.contains("antenna"))
2170 {
2171 return;
2172 }
2173 auto id = parse_json_id(j, "antenna", "Receiver");
2174 auto* ant = world.findAntenna(id);
2175 if (ant == nullptr)
2176 {
2177 throw std::runtime_error("Antenna ID " + std::to_string(id) + " not found.");
2178 }
2179 rx.setAntenna(ant);
2180 }
2181
2183 {
2184 if (!j.contains("timing"))
2185 {
2186 return;
2187 }
2188 auto timing_id = parse_json_id(j, "timing", "Receiver");
2189 auto* const timing_proto = world.findTiming(timing_id);
2190 if (timing_proto == nullptr)
2191 {
2192 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
2193 }
2194 unsigned const seed = rx.getTiming() ? rx.getTiming()->getSeed() : 0;
2195 auto timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
2196 timing->initializeModel(timing_proto);
2197 rx.setTiming(timing);
2198 }
2199
2201 {
2202 if (!j.contains("schedule"))
2203 {
2204 return;
2205 }
2206 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
2207 const bool pulsed = rx.getMode() == radar::OperationMode::PULSED_MODE;
2208 const RealType pri = pulsed ? 1.0 / rx.getWindowPrf() : 0.0;
2209 rx.setSchedule(radar::processRawSchedule(raw, rx.getName(), pulsed, pri));
2210 }
2211
2212 void update_receiver_from_json(const nlohmann::json& j, radar::Receiver* rx, core::World& world,
2213 std::mt19937& /*masterSeeder*/)
2214 {
2215 if (j.contains("name"))
2216 rx->setName(j.at("name").get<std::string>());
2217
2223 if (j.contains("fmcw_mode"))
2224 {
2225 parse_receiver_dechirp_config(j, *rx, "Receiver '" + rx->getName() + "'");
2226 }
2228 }
2229
2230 nlohmann::json monostatic_transmitter_json(const nlohmann::json& j)
2231 {
2232 auto transmitter_json = j;
2233 if (transmitter_json.contains("fmcw_mode") && transmitter_json.at("fmcw_mode").is_object())
2234 {
2235 transmitter_json["fmcw_mode"].erase("dechirp_mode");
2236 transmitter_json["fmcw_mode"].erase("dechirp_reference");
2237 transmitter_json["fmcw_mode"].erase("if_sample_rate");
2238 transmitter_json["fmcw_mode"].erase("if_filter_bandwidth");
2239 transmitter_json["fmcw_mode"].erase("if_filter_transition_width");
2240 }
2241 return transmitter_json;
2242 }
2243
2245 core::World& world)
2246 {
2247 if (j.contains("name"))
2248 rx.setName(j.at("name").get<std::string>());
2249 rx.setMode(tx.getMode());
2250 if (rx.getMode() == radar::OperationMode::PULSED_MODE && j.contains("pulsed_mode"))
2251 {
2252 const auto& mode_json = j.at("pulsed_mode");
2253 rx.setWindowProperties(mode_json.value("window_length", 0.0), tx.getPrf(),
2254 mode_json.value("window_skip", 0.0));
2255 }
2257 if (j.contains("antenna"))
2258 {
2259 rx.setAntenna(world.findAntenna(parse_json_id(j, "antenna", "Monostatic")));
2260 }
2261 }
2262
2264 core::World& world)
2265 {
2266 if (!j.contains("timing"))
2267 {
2268 return;
2269 }
2270 auto timing_id = parse_json_id(j, "timing", "Monostatic");
2271 auto* const timing_proto = world.findTiming(timing_id);
2272 if (timing_proto == nullptr)
2273 {
2274 throw std::runtime_error("Timing ID " + std::to_string(timing_id) + " not found.");
2275 }
2276 unsigned const seed = rx.getTiming() ? rx.getTiming()->getSeed() : 0;
2277 auto shared_timing = std::make_shared<timing::Timing>(timing_proto->getName(), seed, timing_proto->getId());
2278 shared_timing->initializeModel(timing_proto);
2279 tx.setTiming(shared_timing);
2280 rx.setTiming(shared_timing);
2281 }
2282
2284 {
2285 if (!j.contains("schedule"))
2286 {
2287 return;
2288 }
2289 auto raw = j.at("schedule").get<std::vector<radar::SchedulePeriod>>();
2290 const bool pulsed = tx.getMode() == radar::OperationMode::PULSED_MODE;
2291 const RealType pri = pulsed ? 1.0 / tx.getPrf() : 0.0;
2292 auto processed_schedule = radar::processRawSchedule(raw, tx.getName(), pulsed, pri);
2293 if (tx.getSignal() != nullptr && (tx.getSignal()->isFmcwFamily() || tx.getSignal()->isSteppedFrequency()))
2294 {
2295 validate_fmcw_schedule(processed_schedule, *tx.getSignal(), "Monostatic '" + tx.getName() + "'");
2296 }
2297 tx.setSchedule(processed_schedule);
2298 rx.setSchedule(processed_schedule);
2299 }
2300
2302 core::World& world, std::mt19937& masterSeeder)
2303 {
2306
2310 validate_transmitter_signal_state(*tx, "Monostatic '" + tx->getName() + "'");
2311 if (j.contains("fmcw_mode"))
2312 {
2313 parse_receiver_dechirp_config(j, *rx, "Monostatic '" + tx->getName() + "'");
2314 }
2316 }
2317
2319 std::mt19937& /*masterSeeder*/)
2320 {
2321 auto* plat = existing_tgt->getPlatform();
2322 const auto& rcs_json = j.at("rcs");
2323 const auto rcs_type = rcs_json.at("type").get<std::string>();
2324 std::unique_ptr<radar::Target> target_obj;
2325
2326 const auto target_id = existing_tgt->getId();
2327 const auto name = j.value("name", existing_tgt->getName());
2328 unsigned const seed = existing_tgt->getSeed();
2329
2330 if (rcs_type == "isotropic")
2331 {
2332 target_obj = radar::createIsoTarget(plat, name, rcs_json.value("value", 1.0), seed, target_id);
2333 }
2334 else if (rcs_type == "file")
2335 {
2336 const auto filename = rcs_json.value("filename", "");
2338 }
2339 else
2340 {
2341 throw std::runtime_error("Unsupported target RCS type: " + rcs_type);
2342 }
2343
2344 if (j.contains("model"))
2345 {
2346 const auto& model_json = j.at("model");
2347 const auto model_type = model_json.at("type").get<std::string>();
2348 if (model_type == "chisquare" || model_type == "gamma")
2349 {
2350 auto model =
2351 std::make_unique<radar::RcsChiSquare>(target_obj->getRngEngine(), model_json.value("k", 1.0));
2352 target_obj->setFluctuationModel(std::move(model));
2353 }
2354 else if (model_type == "constant")
2355 {
2356 target_obj->setFluctuationModel(std::make_unique<radar::RcsConst>());
2357 }
2358 }
2359
2360 world.replace(std::move(target_obj));
2361 }
2362
2363 void update_timing_from_json(const nlohmann::json& j, core::World& world, const SimId id)
2364 {
2365 auto* existing = world.findTiming(id);
2366 if (existing == nullptr)
2367 {
2368 throw std::runtime_error("Timing ID " + std::to_string(id) + " not found.");
2369 }
2370
2371 auto patched = j;
2372 if (!patched.contains("name"))
2373 {
2374 patched["name"] = existing->getName();
2375 }
2376
2378 }
2379
2380 nlohmann::json world_to_json(const core::World& world)
2381 {
2382 nlohmann::json sim_json;
2383
2385 sim_json["parameters"] = params::params;
2386
2387 sim_json["waveforms"] = nlohmann::json::array();
2388 for (const auto& waveform : world.getWaveforms() | std::views::values)
2389 {
2390 sim_json["waveforms"].push_back(*waveform);
2391 }
2392
2393 sim_json["antennas"] = nlohmann::json::array();
2394 for (const auto& antenna : world.getAntennas() | std::views::values)
2395 {
2396 sim_json["antennas"].push_back(*antenna);
2397 }
2398
2399 sim_json["timings"] = nlohmann::json::array();
2400 for (const auto& timing : world.getTimings() | std::views::values)
2401 {
2402 sim_json["timings"].push_back(*timing);
2403 }
2404
2405 sim_json["platforms"] = nlohmann::json::array();
2406 for (const auto& p : world.getPlatforms())
2407 {
2408 sim_json["platforms"].push_back(serialize_platform(p.get(), world));
2409 }
2410
2411 return {{"simulation", sim_json}};
2412 }
2413
2414 void json_to_world(const nlohmann::json& j, core::World& world, std::mt19937& masterSeeder)
2415 {
2418 world.swap(parsed_world);
2419 }
2420}
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:440
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:282
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:408
void resolveReceiverDechirpReferences()
Resolves and validates receiver FMCW dechirp references after all components are loaded.
Definition world.cpp:553
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:423
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:94
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:89
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:96
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
@ SFCW_MODE
The component operates in a stepped-frequency CW streaming mode.
@ 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< RadarSignal > loadWaveformFromFile(const std::string &name, const std::string &filename, const RealType power, const RealType carrierFreq, const SimId id, const FileWaveformKind kind)
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)
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.