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 == "triangle")
84 {
85 if (fmcw.triangle_period.has_value())
86 {
87 result["triangle_period"] = *fmcw.triangle_period;
88 }
89 if (fmcw.triangle_count.has_value())
90 {
91 result["triangle_count"] = *fmcw.triangle_count;
92 }
93 }
94 return result;
95 }
96
97 /// Converts one FMCW source segment metadata entry to JSON.
98 nlohmann::json fmcwSourceSegmentToJson(const FmcwSourceSegmentMetadata& segment)
99 {
100 nlohmann::json result = {{"start_time", segment.start_time}, {"end_time", segment.end_time}};
101 if (segment.first_chirp_start_time.has_value())
102 {
103 result["first_chirp_start_time"] = *segment.first_chirp_start_time;
104 }
105 if (segment.emitted_chirp_count.has_value())
106 {
107 result["emitted_chirp_count"] = *segment.emitted_chirp_count;
108 }
109 if (segment.first_triangle_start_time.has_value())
110 {
111 result["first_triangle_start_time"] = *segment.first_triangle_start_time;
112 }
113 if (segment.emitted_triangle_count.has_value())
114 {
115 result["emitted_triangle_count"] = *segment.emitted_triangle_count;
116 }
117 return result;
118 }
119
120 /// Converts one FMCW source metadata entry to JSON.
121 nlohmann::json fmcwSourceToJson(const FmcwSourceMetadata& source)
122 {
123 nlohmann::json segments = nlohmann::json::array();
124 for (const auto& segment : source.segments)
125 {
126 segments.push_back(fmcwSourceSegmentToJson(segment));
127 }
128
129 nlohmann::json result = {{"transmitter_id", source.transmitter_id},
130 {"transmitter_name", source.transmitter_name},
131 {"waveform_id", source.waveform_id},
132 {"waveform_name", source.waveform_name},
133 {"carrier_frequency", source.carrier_frequency},
134 {"segments", segments}};
135 result.update(fmcwToJson(source.waveform));
136 return result;
137 }
138
139 nlohmann::json sfcwToJson(const SfcwMetadata& sfcw)
140 {
141 nlohmann::json result = {{"carrier_frequency", sfcw.carrier_frequency},
142 {"start_frequency_offset", sfcw.start_frequency_offset},
143 {"step_size", sfcw.step_size},
144 {"step_count", sfcw.step_count},
145 {"dwell_time", sfcw.dwell_time},
146 {"step_period", sfcw.step_period},
147 {"first_frequency", sfcw.first_frequency},
148 {"last_frequency", sfcw.last_frequency},
149 {"frequency_span", sfcw.frequency_span},
150 {"effective_bandwidth", sfcw.effective_bandwidth},
151 {"range_resolution", sfcw.range_resolution},
152 {"unambiguous_range", sfcw.unambiguous_range}};
153 if (sfcw.sweep_count.has_value())
154 {
155 result["sweep_count"] = *sfcw.sweep_count;
156 }
157 return result;
158 }
159
160 nlohmann::json sfcwSourceSegmentToJson(const SfcwSourceSegmentMetadata& segment)
161 {
162 nlohmann::json result = {{"start_time", segment.start_time}, {"end_time", segment.end_time}};
163 if (segment.first_step_start_time.has_value())
164 {
165 result["first_step_start_time"] = *segment.first_step_start_time;
166 }
167 if (segment.emitted_step_count.has_value())
168 {
169 result["emitted_step_count"] = *segment.emitted_step_count;
170 }
171 return result;
172 }
173
174 nlohmann::json sfcwSourceToJson(const SfcwSourceMetadata& source)
175 {
176 nlohmann::json segments = nlohmann::json::array();
177 for (const auto& segment : source.segments)
178 {
179 segments.push_back(sfcwSourceSegmentToJson(segment));
180 }
181
182 nlohmann::json result = {{"transmitter_id", source.transmitter_id},
183 {"transmitter_name", source.transmitter_name},
184 {"waveform_id", source.waveform_id},
185 {"waveform_name", source.waveform_name},
186 {"segments", segments}};
187 result.update(sfcwToJson(source.waveform));
188 return result;
189 }
190
191 template <typename T>
192 void addOptional(nlohmann::json& result, const char* key, const std::optional<T>& value)
193 {
194 if (value.has_value())
195 {
196 result[key] = *value;
197 }
198 }
199
200 void addDechirpReferenceJson(nlohmann::json& result, const OutputFileMetadata& file)
201 {
202 addOptional(result, "fmcw_dechirp_reference_transmitter_id", file.fmcw_dechirp_reference_transmitter_id);
203 addOptional(result, "fmcw_dechirp_reference_transmitter_name",
204 file.fmcw_dechirp_reference_transmitter_name);
205 addOptional(result, "fmcw_dechirp_reference_waveform_id", file.fmcw_dechirp_reference_waveform_id);
206 addOptional(result, "fmcw_dechirp_reference_waveform_name", file.fmcw_dechirp_reference_waveform_name);
207 if (file.fmcw_dechirp_reference_waveform.has_value())
208 {
209 result["fmcw_dechirp_reference_waveform"] = fmcwToJson(*file.fmcw_dechirp_reference_waveform);
210 }
211 }
212
213 void addFmcwIfJson(nlohmann::json& result, const OutputFileMetadata& file)
214 {
215 result["fmcw_if_decimation_enabled"] = file.fmcw_if_decimation_enabled;
216 result["fmcw_if_legacy_full_rate"] = file.fmcw_if_legacy_full_rate;
217 addOptional(result, "fmcw_if_requested_sample_rate", file.fmcw_if_requested_sample_rate);
218 addOptional(result, "fmcw_if_sample_rate", file.fmcw_if_sample_rate);
219 addOptional(result, "fmcw_if_input_sample_rate", file.fmcw_if_input_sample_rate);
220 addOptional(result, "fmcw_if_resample_numerator", file.fmcw_if_resample_numerator);
221 addOptional(result, "fmcw_if_resample_denominator", file.fmcw_if_resample_denominator);
222 addOptional(result, "fmcw_if_decimation_factor", file.fmcw_if_decimation_factor);
223 addOptional(result, "fmcw_if_filter_bandwidth", file.fmcw_if_filter_bandwidth);
224 addOptional(result, "fmcw_if_filter_transition_width", file.fmcw_if_filter_transition_width);
225 addOptional(result, "fmcw_if_filter_stopband", file.fmcw_if_filter_stopband);
226 addOptional(result, "fmcw_if_filter_group_delay_seconds", file.fmcw_if_filter_group_delay_seconds);
227 addOptional(result, "fmcw_if_compensated_integer_delay_samples",
228 file.fmcw_if_compensated_integer_delay_samples);
229 addOptional(result, "fmcw_if_compensated_fractional_delay_samples",
230 file.fmcw_if_compensated_fractional_delay_samples);
231 addOptional(result, "fmcw_if_warmup_discard_samples", file.fmcw_if_warmup_discard_samples);
232 addOptional(result, "fmcw_if_phase_refinement", file.fmcw_if_phase_refinement);
233 addOptional(result, "fmcw_if_timing_error_seconds", file.fmcw_if_timing_error_seconds);
234 addOptional(result, "fmcw_if_phase_error_radians", file.fmcw_if_phase_error_radians);
235 addOptional(result, "fmcw_if_noise_variance", file.fmcw_if_noise_variance);
236 result["fmcw_if_group_delay_compensated"] = file.fmcw_if_group_delay_compensated;
237 }
238
239 /// Converts one output file metadata entry to JSON.
240 nlohmann::json fileToJson(const OutputFileMetadata& file)
241 {
242 nlohmann::json chunks = nlohmann::json::array();
243 for (const auto& chunk : file.chunks)
244 {
245 chunks.push_back(chunkToJson(chunk));
246 }
247
248 nlohmann::json streaming_segments = nlohmann::json::array();
249 for (const auto& segment : file.streaming_segments)
250 {
251 streaming_segments.push_back(streamingSegmentToJson(segment));
252 }
253
254 nlohmann::json fmcw_sources = nlohmann::json::array();
255 for (const auto& source : file.fmcw_sources)
256 {
257 fmcw_sources.push_back(fmcwSourceToJson(source));
258 }
259
260 nlohmann::json sfcw_sources = nlohmann::json::array();
261 for (const auto& source : file.sfcw_sources)
262 {
263 sfcw_sources.push_back(sfcwSourceToJson(source));
264 }
265
266 nlohmann::json result = {{"receiver_id", file.receiver_id},
267 {"receiver_name", file.receiver_name},
268 {"mode", file.mode},
269 {"path", file.path},
270 {"sampling_rate", file.sampling_rate},
271 {"total_samples", file.total_samples},
272 {"sample_start", file.sample_start},
273 {"sample_end_exclusive", file.sample_end_exclusive},
274 {"pulse_count", file.pulse_count},
275 {"min_pulse_length_samples", file.min_pulse_length_samples},
276 {"max_pulse_length_samples", file.max_pulse_length_samples},
277 {"uniform_pulse_length", file.uniform_pulse_length},
278 {"chunks", chunks},
279 {"streaming_segments", streaming_segments},
280 {"fmcw_sources", fmcw_sources},
281 {"sfcw_sources", sfcw_sources},
282 {"fmcw_dechirp_mode", file.fmcw_dechirp_mode},
283 {"fmcw_dechirp_reference_source", file.fmcw_dechirp_reference_source}};
284 if (file.fmcw.has_value())
285 {
286 result["fmcw"] = fmcwToJson(*file.fmcw);
287 }
288 if (file.sfcw.has_value())
289 {
290 result["sfcw"] = sfcwToJson(*file.sfcw);
291 }
294 return result;
295 }
296
297 /// Converts one VITA stream metadata entry to JSON.
298 nlohmann::json vita49StreamToJson(const Vita49StreamMetadata& stream)
299 {
300 auto timestampToJson = [](const std::optional<Vita49Timestamp>& timestamp) -> nlohmann::json
301 {
302 if (!timestamp.has_value())
303 {
304 return nullptr;
305 }
306 return {{"integer_seconds", timestamp->integer_seconds},
307 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
308 };
309
310 return {{"receiver_id", stream.receiver_id},
311 {"receiver_name", stream.receiver_name},
312 {"stream_id", stream.stream_id},
313 {"mode", stream.mode},
314 {"sample_rate", stream.sample_rate},
315 {"reference_frequency", stream.reference_frequency},
316 {"packets_emitted", stream.packets_emitted},
317 {"samples_emitted", stream.samples_emitted},
318 {"packets_dropped", stream.packets_dropped},
319 {"samples_dropped", stream.samples_dropped},
320 {"over_range_count", stream.over_range_count},
321 {"late_packet_count", stream.late_packet_count},
322 {"context_packet_count", stream.context_packet_count},
323 {"first_sample_time",
324 stream.first_sample_time.has_value() ? nlohmann::json(*stream.first_sample_time)
325 : nlohmann::json(nullptr)},
326 {"end_sample_time",
327 stream.end_sample_time.has_value() ? nlohmann::json(*stream.end_sample_time)
328 : nlohmann::json(nullptr)},
329 {"first_timestamp", timestampToJson(stream.first_timestamp)},
330 {"end_timestamp", timestampToJson(stream.end_timestamp)}};
331 }
332
333 /// Converts VITA output metadata to JSON.
334 nlohmann::json vita49ToJson(const Vita49OutputMetadata& vita49)
335 {
336 nlohmann::json streams = nlohmann::json::array();
337 for (const auto& stream : vita49.streams)
338 {
339 streams.push_back(vita49StreamToJson(stream));
340 }
341
342 nlohmann::json result = {{"endpoint", vita49.endpoint_host + ":" + std::to_string(vita49.endpoint_port)},
343 {"endpoint_host", vita49.endpoint_host},
344 {"endpoint_port", vita49.endpoint_port},
345 {"epoch_unix_nanoseconds", nullptr},
346 {"class_id", vita49.class_id},
347 {"adc_fullscale", vita49.adc_fullscale},
348 {"max_udp_payload", vita49.max_udp_payload},
349 {"queue_depth", vita49.queue_depth},
350 {"streams", streams}};
351 if (vita49.epoch_unix_nanoseconds.has_value())
352 {
353 result["epoch_unix_nanoseconds"] = std::to_string(*vita49.epoch_unix_nanoseconds);
354 }
355 return result;
356 }
357
358 /// Converts a full output metadata snapshot to JSON.
359 nlohmann::json metadataToJson(const OutputMetadata& metadata)
360 {
361 nlohmann::json files = nlohmann::json::array();
362 std::vector<RealType> file_sampling_rates;
363 for (const auto& file : metadata.files)
364 {
365 files.push_back(fileToJson(file));
366 bool already_present = false;
367 for (const auto rate : file_sampling_rates)
368 {
369 if (rate == file.sampling_rate)
370 {
371 already_present = true;
372 break;
373 }
374 }
375 if (!already_present)
376 {
377 file_sampling_rates.push_back(file.sampling_rate);
378 }
379 }
380
381 nlohmann::json result = {{"schema_version", metadata.schema_version},
382 {"simulation_name", metadata.simulation_name},
383 {"output_directory", metadata.output_directory},
384 {"start_time", metadata.start_time},
385 {"end_time", metadata.end_time},
386 {"oversample_ratio", metadata.oversample_ratio},
387 {"files", files}};
388 if (file_sampling_rates.empty())
389 {
390 result["sampling_rate"] = metadata.sampling_rate;
391 }
392 else if (file_sampling_rates.size() == 1)
393 {
394 result["sampling_rate"] = file_sampling_rates.front();
395 }
396 else
397 {
398 result["sampling_rate"] = nullptr;
399 result["sampling_rates"] = file_sampling_rates;
400 }
401 if (metadata.vita49.has_value())
402 {
403 result["vita49"] = vita49ToJson(*metadata.vita49);
404 }
405 return result;
406 }
407 }
408
409 OutputMetadataCollector::OutputMetadataCollector(std::string output_dir)
410 {
411 _metadata.output_directory = std::move(output_dir);
412 _metadata.simulation_name = params::params.simulation_name;
413 _metadata.start_time = params::startTime();
414 _metadata.end_time = params::endTime();
415 _metadata.sampling_rate = params::rate();
416 _metadata.oversample_ratio = params::oversampleRatio();
417 }
418
419 void OutputMetadataCollector::addFile(OutputFileMetadata file_metadata)
420 {
421 std::scoped_lock const lock(_mutex);
422 _metadata.files.push_back(std::move(file_metadata));
423 }
424
425 OutputMetadata OutputMetadataCollector::snapshot() const
426 {
427 std::scoped_lock const lock(_mutex);
428 return _metadata;
429 }
430
432 {
433 return fileToJson(metadata).dump(2);
434 }
435
436 std::string outputMetadataToJsonString(const OutputMetadata& metadata) { return metadataToJson(metadata).dump(2); }
437
439 {
441 .endpoint_port = config.port,
442 .epoch_unix_nanoseconds = config.epoch_unix_nanoseconds,
443 .adc_fullscale = config.adc_fullscale,
444 .max_udp_payload = config.max_udp_payload,
445 .queue_depth = config.queue_depth};
446 }
447}
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