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 return result;
55 }
56
57 /// Converts FMCW output metadata to JSON.
58 nlohmann::json fmcwToJson(const FmcwMetadata& fmcw)
59 {
60 nlohmann::json result = {{"chirp_bandwidth", fmcw.chirp_bandwidth},
61 {"chirp_duration", fmcw.chirp_duration},
62 {"waveform_shape", fmcw.waveform_shape},
63 {"chirp_rate", fmcw.chirp_rate},
64 {"start_frequency_offset", fmcw.start_frequency_offset}};
65 if (fmcw.waveform_shape == "linear")
66 {
67 result["chirp_period"] = fmcw.chirp_period;
68 result["chirp_rate_signed"] = fmcw.chirp_rate_signed;
69 result["chirp_direction"] = fmcw.chirp_direction;
70 if (fmcw.chirp_count.has_value())
71 {
72 result["chirp_count"] = *fmcw.chirp_count;
73 }
74 }
75 else if (fmcw.waveform_shape == "triangle")
76 {
77 if (fmcw.triangle_period.has_value())
78 {
79 result["triangle_period"] = *fmcw.triangle_period;
80 }
81 if (fmcw.triangle_count.has_value())
82 {
83 result["triangle_count"] = *fmcw.triangle_count;
84 }
85 }
86 return result;
87 }
88
89 /// Converts one FMCW source segment metadata entry to JSON.
90 nlohmann::json fmcwSourceSegmentToJson(const FmcwSourceSegmentMetadata& segment)
91 {
92 nlohmann::json result = {{"start_time", segment.start_time}, {"end_time", segment.end_time}};
93 if (segment.first_chirp_start_time.has_value())
94 {
95 result["first_chirp_start_time"] = *segment.first_chirp_start_time;
96 }
97 if (segment.emitted_chirp_count.has_value())
98 {
99 result["emitted_chirp_count"] = *segment.emitted_chirp_count;
100 }
101 if (segment.first_triangle_start_time.has_value())
102 {
103 result["first_triangle_start_time"] = *segment.first_triangle_start_time;
104 }
105 if (segment.emitted_triangle_count.has_value())
106 {
107 result["emitted_triangle_count"] = *segment.emitted_triangle_count;
108 }
109 return result;
110 }
111
112 /// Converts one FMCW source metadata entry to JSON.
113 nlohmann::json fmcwSourceToJson(const FmcwSourceMetadata& source)
114 {
115 nlohmann::json segments = nlohmann::json::array();
116 for (const auto& segment : source.segments)
117 {
118 segments.push_back(fmcwSourceSegmentToJson(segment));
119 }
120
121 nlohmann::json result = {{"transmitter_id", source.transmitter_id},
122 {"transmitter_name", source.transmitter_name},
123 {"waveform_id", source.waveform_id},
124 {"waveform_name", source.waveform_name},
125 {"carrier_frequency", source.carrier_frequency},
126 {"segments", segments}};
127 result.update(fmcwToJson(source.waveform));
128 return result;
129 }
130
131 template <typename T>
132 void addOptional(nlohmann::json& result, const char* key, const std::optional<T>& value)
133 {
134 if (value.has_value())
135 {
136 result[key] = *value;
137 }
138 }
139
140 void addDechirpReferenceJson(nlohmann::json& result, const OutputFileMetadata& file)
141 {
142 addOptional(result, "fmcw_dechirp_reference_transmitter_id", file.fmcw_dechirp_reference_transmitter_id);
143 addOptional(result, "fmcw_dechirp_reference_transmitter_name",
144 file.fmcw_dechirp_reference_transmitter_name);
145 addOptional(result, "fmcw_dechirp_reference_waveform_id", file.fmcw_dechirp_reference_waveform_id);
146 addOptional(result, "fmcw_dechirp_reference_waveform_name", file.fmcw_dechirp_reference_waveform_name);
147 if (file.fmcw_dechirp_reference_waveform.has_value())
148 {
149 result["fmcw_dechirp_reference_waveform"] = fmcwToJson(*file.fmcw_dechirp_reference_waveform);
150 }
151 }
152
153 void addFmcwIfJson(nlohmann::json& result, const OutputFileMetadata& file)
154 {
155 result["fmcw_if_decimation_enabled"] = file.fmcw_if_decimation_enabled;
156 result["fmcw_if_legacy_full_rate"] = file.fmcw_if_legacy_full_rate;
157 addOptional(result, "fmcw_if_requested_sample_rate", file.fmcw_if_requested_sample_rate);
158 addOptional(result, "fmcw_if_sample_rate", file.fmcw_if_sample_rate);
159 addOptional(result, "fmcw_if_input_sample_rate", file.fmcw_if_input_sample_rate);
160 addOptional(result, "fmcw_if_resample_numerator", file.fmcw_if_resample_numerator);
161 addOptional(result, "fmcw_if_resample_denominator", file.fmcw_if_resample_denominator);
162 addOptional(result, "fmcw_if_decimation_factor", file.fmcw_if_decimation_factor);
163 addOptional(result, "fmcw_if_filter_bandwidth", file.fmcw_if_filter_bandwidth);
164 addOptional(result, "fmcw_if_filter_transition_width", file.fmcw_if_filter_transition_width);
165 addOptional(result, "fmcw_if_filter_stopband", file.fmcw_if_filter_stopband);
166 addOptional(result, "fmcw_if_filter_group_delay_seconds", file.fmcw_if_filter_group_delay_seconds);
167 addOptional(result, "fmcw_if_compensated_integer_delay_samples",
168 file.fmcw_if_compensated_integer_delay_samples);
169 addOptional(result, "fmcw_if_compensated_fractional_delay_samples",
170 file.fmcw_if_compensated_fractional_delay_samples);
171 addOptional(result, "fmcw_if_warmup_discard_samples", file.fmcw_if_warmup_discard_samples);
172 addOptional(result, "fmcw_if_phase_refinement", file.fmcw_if_phase_refinement);
173 addOptional(result, "fmcw_if_timing_error_seconds", file.fmcw_if_timing_error_seconds);
174 addOptional(result, "fmcw_if_phase_error_radians", file.fmcw_if_phase_error_radians);
175 addOptional(result, "fmcw_if_noise_variance", file.fmcw_if_noise_variance);
176 result["fmcw_if_group_delay_compensated"] = file.fmcw_if_group_delay_compensated;
177 }
178
179 /// Converts one output file metadata entry to JSON.
180 nlohmann::json fileToJson(const OutputFileMetadata& file)
181 {
182 nlohmann::json chunks = nlohmann::json::array();
183 for (const auto& chunk : file.chunks)
184 {
185 chunks.push_back(chunkToJson(chunk));
186 }
187
188 nlohmann::json streaming_segments = nlohmann::json::array();
189 for (const auto& segment : file.streaming_segments)
190 {
191 streaming_segments.push_back(streamingSegmentToJson(segment));
192 }
193
194 nlohmann::json fmcw_sources = nlohmann::json::array();
195 for (const auto& source : file.fmcw_sources)
196 {
197 fmcw_sources.push_back(fmcwSourceToJson(source));
198 }
199
200 nlohmann::json result = {{"receiver_id", file.receiver_id},
201 {"receiver_name", file.receiver_name},
202 {"mode", file.mode},
203 {"path", file.path},
204 {"sampling_rate", file.sampling_rate},
205 {"total_samples", file.total_samples},
206 {"sample_start", file.sample_start},
207 {"sample_end_exclusive", file.sample_end_exclusive},
208 {"pulse_count", file.pulse_count},
209 {"min_pulse_length_samples", file.min_pulse_length_samples},
210 {"max_pulse_length_samples", file.max_pulse_length_samples},
211 {"uniform_pulse_length", file.uniform_pulse_length},
212 {"chunks", chunks},
213 {"streaming_segments", streaming_segments},
214 {"fmcw_sources", fmcw_sources},
215 {"fmcw_dechirp_mode", file.fmcw_dechirp_mode},
216 {"fmcw_dechirp_reference_source", file.fmcw_dechirp_reference_source}};
217 if (file.fmcw.has_value())
218 {
219 result["fmcw"] = fmcwToJson(*file.fmcw);
220 }
223 return result;
224 }
225
226 /// Converts one VITA stream metadata entry to JSON.
227 nlohmann::json vita49StreamToJson(const Vita49StreamMetadata& stream)
228 {
229 auto timestampToJson = [](const std::optional<Vita49Timestamp>& timestamp) -> nlohmann::json
230 {
231 if (!timestamp.has_value())
232 {
233 return nullptr;
234 }
235 return {{"integer_seconds", timestamp->integer_seconds},
236 {"fractional_picoseconds", timestamp->fractional_picoseconds}};
237 };
238
239 return {{"receiver_id", stream.receiver_id},
240 {"receiver_name", stream.receiver_name},
241 {"stream_id", stream.stream_id},
242 {"mode", stream.mode},
243 {"sample_rate", stream.sample_rate},
244 {"reference_frequency", stream.reference_frequency},
245 {"packets_emitted", stream.packets_emitted},
246 {"samples_emitted", stream.samples_emitted},
247 {"packets_dropped", stream.packets_dropped},
248 {"samples_dropped", stream.samples_dropped},
249 {"over_range_count", stream.over_range_count},
250 {"late_packet_count", stream.late_packet_count},
251 {"context_packet_count", stream.context_packet_count},
252 {"first_sample_time",
253 stream.first_sample_time.has_value() ? nlohmann::json(*stream.first_sample_time)
254 : nlohmann::json(nullptr)},
255 {"end_sample_time",
256 stream.end_sample_time.has_value() ? nlohmann::json(*stream.end_sample_time)
257 : nlohmann::json(nullptr)},
258 {"first_timestamp", timestampToJson(stream.first_timestamp)},
259 {"end_timestamp", timestampToJson(stream.end_timestamp)}};
260 }
261
262 /// Converts VITA output metadata to JSON.
263 nlohmann::json vita49ToJson(const Vita49OutputMetadata& vita49)
264 {
265 nlohmann::json streams = nlohmann::json::array();
266 for (const auto& stream : vita49.streams)
267 {
268 streams.push_back(vita49StreamToJson(stream));
269 }
270
271 nlohmann::json result = {{"endpoint", vita49.endpoint_host + ":" + std::to_string(vita49.endpoint_port)},
272 {"endpoint_host", vita49.endpoint_host},
273 {"endpoint_port", vita49.endpoint_port},
274 {"epoch_unix_nanoseconds", nullptr},
275 {"class_id", vita49.class_id},
276 {"adc_fullscale", vita49.adc_fullscale},
277 {"max_udp_payload", vita49.max_udp_payload},
278 {"queue_depth", vita49.queue_depth},
279 {"streams", streams}};
280 if (vita49.epoch_unix_nanoseconds.has_value())
281 {
282 result["epoch_unix_nanoseconds"] = std::to_string(*vita49.epoch_unix_nanoseconds);
283 }
284 return result;
285 }
286
287 /// Converts a full output metadata snapshot to JSON.
288 nlohmann::json metadataToJson(const OutputMetadata& metadata)
289 {
290 nlohmann::json files = nlohmann::json::array();
291 std::vector<RealType> file_sampling_rates;
292 for (const auto& file : metadata.files)
293 {
294 files.push_back(fileToJson(file));
295 bool already_present = false;
296 for (const auto rate : file_sampling_rates)
297 {
298 if (rate == file.sampling_rate)
299 {
300 already_present = true;
301 break;
302 }
303 }
304 if (!already_present)
305 {
306 file_sampling_rates.push_back(file.sampling_rate);
307 }
308 }
309
310 nlohmann::json result = {{"schema_version", metadata.schema_version},
311 {"simulation_name", metadata.simulation_name},
312 {"output_directory", metadata.output_directory},
313 {"start_time", metadata.start_time},
314 {"end_time", metadata.end_time},
315 {"oversample_ratio", metadata.oversample_ratio},
316 {"files", files}};
317 if (file_sampling_rates.empty())
318 {
319 result["sampling_rate"] = metadata.sampling_rate;
320 }
321 else if (file_sampling_rates.size() == 1)
322 {
323 result["sampling_rate"] = file_sampling_rates.front();
324 }
325 else
326 {
327 result["sampling_rate"] = nullptr;
328 result["sampling_rates"] = file_sampling_rates;
329 }
330 if (metadata.vita49.has_value())
331 {
332 result["vita49"] = vita49ToJson(*metadata.vita49);
333 }
334 return result;
335 }
336 }
337
338 OutputMetadataCollector::OutputMetadataCollector(std::string output_dir)
339 {
340 _metadata.output_directory = std::move(output_dir);
341 _metadata.simulation_name = params::params.simulation_name;
342 _metadata.start_time = params::startTime();
343 _metadata.end_time = params::endTime();
344 _metadata.sampling_rate = params::rate();
345 _metadata.oversample_ratio = params::oversampleRatio();
346 }
347
348 void OutputMetadataCollector::addFile(OutputFileMetadata file_metadata)
349 {
350 std::scoped_lock const lock(_mutex);
351 _metadata.files.push_back(std::move(file_metadata));
352 }
353
354 OutputMetadata OutputMetadataCollector::snapshot() const
355 {
356 std::scoped_lock const lock(_mutex);
357 return _metadata;
358 }
359
361 {
362 return fileToJson(metadata).dump(2);
363 }
364
365 std::string outputMetadataToJsonString(const OutputMetadata& metadata) { return metadataToJson(metadata).dump(2); }
366
368 {
370 .endpoint_port = config.port,
371 .epoch_unix_nanoseconds = config.epoch_unix_nanoseconds,
372 .adc_fullscale = config.adc_fullscale,
373 .max_udp_payload = config.max_udp_payload,
374 .queue_depth = config.queue_depth};
375 }
376}
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