FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
output_metadata.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7#include "output_metadata.h"
8
9#include <nlohmann/json.hpp>
10#include <utility>
11
12#include "core/parameters.h"
13
14namespace core
15{
16 namespace
17 {
18 /// Converts one pulsed chunk metadata entry to JSON.
19 nlohmann::json chunkToJson(const PulseChunkMetadata& chunk)
20 {
21 return {{"chunk_index", chunk.chunk_index},
22 {"i_dataset", chunk.i_dataset},
23 {"q_dataset", chunk.q_dataset},
24 {"start_time", chunk.start_time},
25 {"sample_count", chunk.sample_count},
26 {"sample_start", chunk.sample_start},
27 {"sample_end_exclusive", chunk.sample_end_exclusive}};
28 }
29
30 /// Converts one streaming segment metadata entry to JSON.
31 nlohmann::json streamingSegmentToJson(const StreamingSegmentMetadata& segment)
32 {
33 nlohmann::json result = {{"start_time", segment.start_time},
34 {"end_time", segment.end_time},
35 {"sample_count", segment.sample_count},
36 {"sample_start", segment.sample_start},
37 {"sample_end_exclusive", segment.sample_end_exclusive}};
38 if (segment.first_chirp_start_time.has_value())
39 {
40 result["first_chirp_start_time"] = *segment.first_chirp_start_time;
41 }
42 if (segment.emitted_chirp_count.has_value())
43 {
44 result["emitted_chirp_count"] = *segment.emitted_chirp_count;
45 }
46 if (segment.first_triangle_start_time.has_value())
47 {
48 result["first_triangle_start_time"] = *segment.first_triangle_start_time;
49 }
50 if (segment.emitted_triangle_count.has_value())
51 {
52 result["emitted_triangle_count"] = *segment.emitted_triangle_count;
53 }
54 if (segment.first_sfcw_step_start_time.has_value())
55 {
56 result["first_sfcw_step_start_time"] = *segment.first_sfcw_step_start_time;
57 }
58 if (segment.emitted_sfcw_step_count.has_value())
59 {
60 result["emitted_sfcw_step_count"] = *segment.emitted_sfcw_step_count;
61 }
62 return result;
63 }
64
65 /// Converts FMCW output metadata to JSON.
66 nlohmann::json fmcwToJson(const FmcwMetadata& fmcw)
67 {
68 nlohmann::json result = {{"chirp_bandwidth", fmcw.chirp_bandwidth},
69 {"chirp_duration", fmcw.chirp_duration},
70 {"waveform_shape", fmcw.waveform_shape},
71 {"chirp_rate", fmcw.chirp_rate},
72 {"start_frequency_offset", fmcw.start_frequency_offset}};
73 if (fmcw.waveform_shape == "linear")
74 {
75 result["chirp_period"] = fmcw.chirp_period;
76 result["chirp_rate_signed"] = fmcw.chirp_rate_signed;
77 result["chirp_direction"] = fmcw.chirp_direction;
78 if (fmcw.chirp_count.has_value())
79 {
80 result["chirp_count"] = *fmcw.chirp_count;
81 }
82 }
83 else if (fmcw.waveform_shape == "file")
84 {
85 if (fmcw.sampled_duration.has_value())
86 {
87 result["sampled_duration"] = *fmcw.sampled_duration;
88 }
89 if (fmcw.sampled_count.has_value())
90 {
91 result["sampled_count"] = *fmcw.sampled_count;
92 }
93 }
94 else if (fmcw.waveform_shape == "triangle")
95 {
96 if (fmcw.triangle_period.has_value())
97 {
98 result["triangle_period"] = *fmcw.triangle_period;
99 }
100 if (fmcw.triangle_count.has_value())
101 {
102 result["triangle_count"] = *fmcw.triangle_count;
103 }
104 }
105 return result;
106 }
107
108 /// Converts one FMCW source segment metadata entry to JSON.
109 nlohmann::json fmcwSourceSegmentToJson(const FmcwSourceSegmentMetadata& segment)
110 {
111 nlohmann::json result = {{"start_time", segment.start_time}, {"end_time", segment.end_time}};
112 if (segment.first_chirp_start_time.has_value())
113 {
114 result["first_chirp_start_time"] = *segment.first_chirp_start_time;
115 }
116 if (segment.emitted_chirp_count.has_value())
117 {
118 result["emitted_chirp_count"] = *segment.emitted_chirp_count;
119 }
120 if (segment.first_triangle_start_time.has_value())
121 {
122 result["first_triangle_start_time"] = *segment.first_triangle_start_time;
123 }
124 if (segment.emitted_triangle_count.has_value())
125 {
126 result["emitted_triangle_count"] = *segment.emitted_triangle_count;
127 }
128 return result;
129 }
130
131 /// Converts one FMCW source metadata entry to JSON.
132 nlohmann::json fmcwSourceToJson(const FmcwSourceMetadata& source)
133 {
134 nlohmann::json segments = nlohmann::json::array();
135 for (const auto& segment : source.segments)
136 {
137 segments.push_back(fmcwSourceSegmentToJson(segment));
138 }
139
140 nlohmann::json result = {{"transmitter_id", source.transmitter_id},
141 {"transmitter_name", source.transmitter_name},
142 {"waveform_id", source.waveform_id},
143 {"waveform_name", source.waveform_name},
144 {"carrier_frequency", source.carrier_frequency},
145 {"segments", segments}};
146 result.update(fmcwToJson(source.waveform));
147 return result;
148 }
149
150 nlohmann::json sfcwToJson(const SfcwMetadata& sfcw)
151 {
152 nlohmann::json result = {{"carrier_frequency", sfcw.carrier_frequency},
153 {"start_frequency_offset", sfcw.start_frequency_offset},
154 {"step_size", sfcw.step_size},
155 {"step_count", sfcw.step_count},
156 {"dwell_time", sfcw.dwell_time},
157 {"step_period", sfcw.step_period},
158 {"first_frequency", sfcw.first_frequency},
159 {"last_frequency", sfcw.last_frequency},
160 {"frequency_span", sfcw.frequency_span},
161 {"effective_bandwidth", sfcw.effective_bandwidth},
162 {"range_resolution", sfcw.range_resolution},
163 {"unambiguous_range", sfcw.unambiguous_range}};
164 if (sfcw.sweep_count.has_value())
165 {
166 result["sweep_count"] = *sfcw.sweep_count;
167 }
168 return result;
169 }
170
171 nlohmann::json sfcwSourceSegmentToJson(const SfcwSourceSegmentMetadata& segment)
172 {
173 nlohmann::json result = {{"start_time", segment.start_time}, {"end_time", segment.end_time}};
174 if (segment.first_step_start_time.has_value())
175 {
176 result["first_step_start_time"] = *segment.first_step_start_time;
177 }
178 if (segment.emitted_step_count.has_value())
179 {
180 result["emitted_step_count"] = *segment.emitted_step_count;
181 }
182 return result;
183 }
184
185 nlohmann::json sfcwSourceToJson(const SfcwSourceMetadata& source)
186 {
187 nlohmann::json segments = nlohmann::json::array();
188 for (const auto& segment : source.segments)
189 {
190 segments.push_back(sfcwSourceSegmentToJson(segment));
191 }
192
193 nlohmann::json result = {{"transmitter_id", source.transmitter_id},
194 {"transmitter_name", source.transmitter_name},
195 {"waveform_id", source.waveform_id},
196 {"waveform_name", source.waveform_name},
197 {"segments", segments}};
198 result.update(sfcwToJson(source.waveform));
199 return result;
200 }
201
202 template <typename T>
203 void addOptional(nlohmann::json& result, const char* key, const std::optional<T>& value)
204 {
205 if (value.has_value())
206 {
207 result[key] = *value;
208 }
209 }
210
211 void addDechirpReferenceJson(nlohmann::json& result, const OutputFileMetadata& file)
212 {
213 addOptional(result, "fmcw_dechirp_reference_transmitter_id", file.fmcw_dechirp_reference_transmitter_id);
214 addOptional(result, "fmcw_dechirp_reference_transmitter_name",
215 file.fmcw_dechirp_reference_transmitter_name);
216 addOptional(result, "fmcw_dechirp_reference_waveform_id", file.fmcw_dechirp_reference_waveform_id);
217 addOptional(result, "fmcw_dechirp_reference_waveform_name", file.fmcw_dechirp_reference_waveform_name);
218 if (file.fmcw_dechirp_reference_waveform.has_value())
219 {
220 result["fmcw_dechirp_reference_waveform"] = fmcwToJson(*file.fmcw_dechirp_reference_waveform);
221 }
222 }
223
224 void addFmcwIfJson(nlohmann::json& result, const OutputFileMetadata& file)
225 {
226 result["fmcw_if_decimation_enabled"] = file.fmcw_if_decimation_enabled;
227 result["fmcw_if_legacy_full_rate"] = file.fmcw_if_legacy_full_rate;
228 addOptional(result, "fmcw_if_requested_sample_rate", file.fmcw_if_requested_sample_rate);
229 addOptional(result, "fmcw_if_sample_rate", file.fmcw_if_sample_rate);
230 addOptional(result, "fmcw_if_input_sample_rate", file.fmcw_if_input_sample_rate);
231 addOptional(result, "fmcw_if_resample_numerator", file.fmcw_if_resample_numerator);
232 addOptional(result, "fmcw_if_resample_denominator", file.fmcw_if_resample_denominator);
233 addOptional(result, "fmcw_if_decimation_factor", file.fmcw_if_decimation_factor);
234 addOptional(result, "fmcw_if_filter_bandwidth", file.fmcw_if_filter_bandwidth);
235 addOptional(result, "fmcw_if_filter_transition_width", file.fmcw_if_filter_transition_width);
236 addOptional(result, "fmcw_if_filter_stopband", file.fmcw_if_filter_stopband);
237 addOptional(result, "fmcw_if_filter_group_delay_seconds", file.fmcw_if_filter_group_delay_seconds);
238 addOptional(result, "fmcw_if_compensated_integer_delay_samples",
239 file.fmcw_if_compensated_integer_delay_samples);
240 addOptional(result, "fmcw_if_compensated_fractional_delay_samples",
241 file.fmcw_if_compensated_fractional_delay_samples);
242 addOptional(result, "fmcw_if_warmup_discard_samples", file.fmcw_if_warmup_discard_samples);
243 addOptional(result, "fmcw_if_phase_refinement", file.fmcw_if_phase_refinement);
244 addOptional(result, "fmcw_if_timing_error_seconds", file.fmcw_if_timing_error_seconds);
245 addOptional(result, "fmcw_if_phase_error_radians", file.fmcw_if_phase_error_radians);
246 addOptional(result, "fmcw_if_noise_variance", file.fmcw_if_noise_variance);
247 result["fmcw_if_group_delay_compensated"] = file.fmcw_if_group_delay_compensated;
248 }
249
250 /// Converts one output file metadata entry to JSON.
251 nlohmann::json fileToJson(const OutputFileMetadata& file)
252 {
253 nlohmann::json chunks = nlohmann::json::array();
254 for (const auto& chunk : file.chunks)
255 {
256 chunks.push_back(chunkToJson(chunk));
257 }
258
259 nlohmann::json streaming_segments = nlohmann::json::array();
260 for (const auto& segment : file.streaming_segments)
261 {
262 streaming_segments.push_back(streamingSegmentToJson(segment));
263 }
264
265 nlohmann::json fmcw_sources = nlohmann::json::array();
266 for (const auto& source : file.fmcw_sources)
267 {
268 fmcw_sources.push_back(fmcwSourceToJson(source));
269 }
270
271 nlohmann::json sfcw_sources = nlohmann::json::array();
272 for (const auto& source : file.sfcw_sources)
273 {
274 sfcw_sources.push_back(sfcwSourceToJson(source));
275 }
276
277 nlohmann::json result = {{"receiver_id", file.receiver_id},
278 {"receiver_name", file.receiver_name},
279 {"mode", file.mode},
280 {"path", file.path},
281 {"sampling_rate", file.sampling_rate},
282 {"total_samples", file.total_samples},
283 {"sample_start", file.sample_start},
284 {"sample_end_exclusive", file.sample_end_exclusive},
285 {"pulse_count", file.pulse_count},
286 {"min_pulse_length_samples", file.min_pulse_length_samples},
287 {"max_pulse_length_samples", file.max_pulse_length_samples},
288 {"uniform_pulse_length", file.uniform_pulse_length},
289 {"chunks", chunks},
290 {"streaming_segments", streaming_segments},
291 {"fmcw_sources", fmcw_sources},
292 {"sfcw_sources", sfcw_sources},
293 {"fmcw_dechirp_mode", file.fmcw_dechirp_mode},
294 {"fmcw_dechirp_reference_source", file.fmcw_dechirp_reference_source}};
295 if (file.fmcw.has_value())
296 {
297 result["fmcw"] = fmcwToJson(*file.fmcw);
298 }
299 if (file.sfcw.has_value())
300 {
301 result["sfcw"] = sfcwToJson(*file.sfcw);
302 }
304 addFmcwIfJson(result, file);
305 return result;
306 }
307
308 /// Converts one VITA stream metadata entry to JSON.
309 nlohmann::json vita49StreamToJson(const Vita49StreamMetadata& stream)
310 {
311 auto timestampToJson = [](const std::optional<Vita49Timestamp>& timestamp) -> nlohmann::json
312 {
313 if (!timestamp.has_value())
314 {
315 return nullptr;
316 }
317 return {{"integer_seconds", timestamp->integer_seconds},
318 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
319 };
320
321 return {{"receiver_id", stream.receiver_id},
322 {"receiver_name", stream.receiver_name},
323 {"stream_id", stream.stream_id},
324 {"mode", stream.mode},
325 {"sample_rate", stream.sample_rate},
326 {"reference_frequency", stream.reference_frequency},
327 {"packets_emitted", stream.packets_emitted},
328 {"samples_emitted", stream.samples_emitted},
329 {"packets_dropped", stream.packets_dropped},
330 {"samples_dropped", stream.samples_dropped},
331 {"over_range_count", stream.over_range_count},
332 {"late_data_packet_count", stream.late_data_packet_count},
333 {"late_context_packet_count", stream.late_context_packet_count},
334 {"context_packet_count", stream.context_packet_count},
335 {"first_sample_time",
336 stream.first_sample_time.has_value() ? nlohmann::json(*stream.first_sample_time)
337 : nlohmann::json(nullptr)},
338 {"end_sample_time",
339 stream.end_sample_time.has_value() ? nlohmann::json(*stream.end_sample_time)
340 : nlohmann::json(nullptr)},
341 {"first_timestamp", timestampToJson(stream.first_timestamp)},
342 {"end_timestamp", timestampToJson(stream.end_timestamp)}};
343 }
344
345 /// Converts VITA output metadata to JSON.
346 nlohmann::json vita49ToJson(const Vita49OutputMetadata& vita49)
347 {
348 nlohmann::json streams = nlohmann::json::array();
349 for (const auto& stream : vita49.streams)
350 {
351 streams.push_back(vita49StreamToJson(stream));
352 }
353
354 nlohmann::json result = {{"endpoint", vita49.endpoint_host + ":" + std::to_string(vita49.endpoint_port)},
355 {"endpoint_host", vita49.endpoint_host},
356 {"endpoint_port", vita49.endpoint_port},
357 {"epoch_unix_nanoseconds", nullptr},
358 {"class_id", vita49.class_id},
359 {"adc_fullscale", vita49.adc_fullscale},
360 {"max_udp_payload", vita49.max_udp_payload},
361 {"queue_depth", vita49.queue_depth},
362 {"streams", streams}};
363 if (vita49.epoch_unix_nanoseconds.has_value())
364 {
365 result["epoch_unix_nanoseconds"] = std::to_string(*vita49.epoch_unix_nanoseconds);
366 }
367 return result;
368 }
369
370 /// Converts a full output metadata snapshot to JSON.
371 nlohmann::json metadataToJson(const OutputMetadata& metadata)
372 {
373 nlohmann::json files = nlohmann::json::array();
374 std::vector<RealType> file_sampling_rates;
375 for (const auto& file : metadata.files)
376 {
377 files.push_back(fileToJson(file));
378 bool already_present = false;
379 for (const auto rate : file_sampling_rates)
380 {
381 if (rate == file.sampling_rate)
382 {
383 already_present = true;
384 break;
385 }
386 }
387 if (!already_present)
388 {
389 file_sampling_rates.push_back(file.sampling_rate);
390 }
391 }
392
393 nlohmann::json result = {{"schema_version", metadata.schema_version},
394 {"simulation_name", metadata.simulation_name},
395 {"output_directory", metadata.output_directory},
396 {"start_time", metadata.start_time},
397 {"end_time", metadata.end_time},
398 {"oversample_ratio", metadata.oversample_ratio},
399 {"files", files}};
400 if (file_sampling_rates.empty())
401 {
402 result["sampling_rate"] = metadata.sampling_rate;
403 }
404 else if (file_sampling_rates.size() == 1)
405 {
406 result["sampling_rate"] = file_sampling_rates.front();
407 }
408 else
409 {
410 result["sampling_rate"] = nullptr;
411 result["sampling_rates"] = file_sampling_rates;
412 }
413 if (metadata.vita49.has_value())
414 {
415 result["vita49"] = vita49ToJson(*metadata.vita49);
416 }
417 return result;
418 }
419 }
420
421 OutputMetadataCollector::OutputMetadataCollector(std::string output_dir)
422 {
423 _metadata.output_directory = std::move(output_dir);
424 _metadata.simulation_name = params::params.simulation_name;
425 _metadata.start_time = params::startTime();
426 _metadata.end_time = params::endTime();
427 _metadata.sampling_rate = params::rate();
428 _metadata.oversample_ratio = params::oversampleRatio();
429 }
430
431 void OutputMetadataCollector::addFile(OutputFileMetadata file_metadata)
432 {
433 std::scoped_lock const lock(_mutex);
434 _metadata.files.push_back(std::move(file_metadata));
435 }
436
437 OutputMetadata OutputMetadataCollector::snapshot() const
438 {
439 std::scoped_lock const lock(_mutex);
440 return _metadata;
441 }
442
444 {
445 return fileToJson(metadata).dump(2);
446 }
447
448 std::string outputMetadataToJsonString(const OutputMetadata& metadata) { return metadataToJson(metadata).dump(2); }
449
451 {
453 .endpoint_port = config.port,
454 .epoch_unix_nanoseconds = config.epoch_unix_nanoseconds,
455 .adc_fullscale = config.adc_fullscale,
456 .max_udp_payload = config.max_udp_payload,
457 .queue_depth = config.queue_depth};
458 }
459}
Vita49OutputMetadata vita49MetadataFromConfig(const Vita49OutputConfig &config)
Builds the static VITA metadata section from runtime output configuration.
std::string outputFileMetadataToJsonString(const OutputFileMetadata &metadata)
Serializes one output-file metadata entry to JSON.
std::string outputMetadataToJsonString(const OutputMetadata &metadata)
Serializes a full simulation output metadata snapshot to JSON.
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
Parameters params
Global simulation parameter state.
Definition parameters.h:85
Defines the Parameters struct and provides methods for managing simulation parameters.
math::Vec3 max
Metadata for one receiver output file.
Metadata summary for the full simulation output set.
Metadata for the VITA 49.2 UDP output backend.
std::string endpoint_host
Destination host.
std::string simulation_name
The name of the simulation, from the XML.
Definition parameters.h:74