FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
antenna_factory.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file antenna_factory.cpp
10 * @brief Implementation of the Antenna class and its derived classes.
11 */
12
14
15#include <algorithm>
16#include <cctype>
17#include <cmath>
18#include <complex>
19#include <cstdint>
20#include <limits>
21#include <optional>
22#include <stdexcept>
23
24#include "antenna_pattern_dtd.h"
25#include "antenna_pattern_xsd.h"
26#include "core/config.h"
27#include "core/logging.h"
28#include "core/portable_utils.h"
29#include "math/geometry_ops.h"
31
32using logging::Level;
33using math::SVec3;
34using math::Vec3;
35
36namespace
37{
38 /// Unit used for XML antenna axis sample angles.
39 enum class AxisUnit : std::uint8_t
40 {
41 Radians, ///< Axis samples are expressed in radians.
42 Degrees, ///< Axis samples are expressed in degrees.
43 };
44
45 /// Gain format used for XML antenna axis sample values.
46 enum class AxisGainFormat : std::uint8_t
47 {
48 Linear, ///< Gain samples are already linear.
49 DBi, ///< Gain samples are expressed in dBi.
50 };
51
52 /// Parsed metadata attributes for one XML antenna axis.
53 struct AxisMetadata
54 {
55 AxisUnit unit{AxisUnit::Radians}; ///< Parsed angle unit.
56 AxisGainFormat format{AxisGainFormat::Linear}; ///< Parsed gain format.
58 bool unit_explicit{}; ///< True when unit was explicitly set.
59 bool format_explicit{}; ///< True when format was explicitly set.
60 bool symmetry_explicit{}; ///< True when symmetry was explicitly set.
61 };
62
63 /// Result from loading one XML antenna gain axis.
64 struct AxisLoadResult
65 {
66 RealType max_gain{}; ///< Maximum linear gain found on the axis.
68 antenna::XmlAntenna::AxisSymmetry::Mirrored}; ///< Axis symmetry mode.
69 std::size_t sample_count{}; ///< Number of parsed samples.
70 };
71
72 struct AxisSampleStats
73 {
74 RealType min_angle{std::numeric_limits<RealType>::max()};
75 RealType max_angle{std::numeric_limits<RealType>::lowest()};
76 RealType max_gain{};
77 std::size_t sample_count{};
78 };
79
80 /// Returns a lowercase copy of a string.
81 std::string toLowerCopy(std::string value)
82 {
83 std::ranges::transform(value, value.begin(),
84 [](const unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
85 return value;
86 }
87
88 /// Parses a finite real value and reports the provided context on failure.
89 RealType parseRealValue(const std::string_view text, const std::string_view context)
90 {
91 const std::string value(text);
92 size_t index = 0;
93 double parsed = 0.0;
94
95 try
96 {
97 parsed = std::stod(value, &index);
98 }
99 catch (const std::exception&)
100 {
101 throw std::runtime_error("Invalid numeric value for " + std::string(context) + ": '" + value + "'.");
102 }
103
104 while (index < value.size() && (std::isspace(static_cast<unsigned char>(value[index])) != 0))
105 {
106 ++index;
107 }
108
109 if (index != value.size())
110 {
111 throw std::runtime_error("Invalid numeric value for " + std::string(context) + ": '" + value + "'.");
112 }
113
114 if (!std::isfinite(parsed))
115 {
116 throw std::runtime_error("Non-finite numeric value for " + std::string(context) + ".");
117 }
118
119 return static_cast<RealType>(parsed);
120 }
121
122 /// Returns the XML token for an axis unit.
123 const char* axisUnitName(const AxisUnit unit) noexcept { return unit == AxisUnit::Degrees ? "deg" : "rad"; }
124
125 /// Returns the XML token for an axis gain format.
126 const char* axisFormatName(const AxisGainFormat format) noexcept
127 {
128 return format == AxisGainFormat::DBi ? "dBi" : "linear";
129 }
130
131 /// Returns the XML token for an axis symmetry mode.
132 const char* axisSymmetryName(const antenna::XmlAntenna::AxisSymmetry symmetry) noexcept
133 {
134 return symmetry == antenna::XmlAntenna::AxisSymmetry::None ? "none" : "mirrored";
135 }
136
137 /// Parses optional metadata attributes from one XML antenna axis.
138 AxisMetadata parseAxisMetadata(const XmlElement& axisXml)
139 {
140 AxisMetadata metadata;
141 const std::string axis_name(axisXml.name());
142
143 if (const auto unit_attr = XmlElement::getOptionalAttribute(axisXml, "unit"); unit_attr.has_value())
144 {
145 metadata.unit_explicit = true;
146 const std::string value = toLowerCopy(*unit_attr);
147 if (value == "rad")
148 {
149 metadata.unit = AxisUnit::Radians;
150 }
151 else if (value == "deg")
152 {
153 metadata.unit = AxisUnit::Degrees;
154 }
155 else
156 {
157 throw std::runtime_error("Unsupported unit '" + *unit_attr + "' on <" + axis_name + "> axis.");
158 }
159 }
160
161 if (const auto format_attr = XmlElement::getOptionalAttribute(axisXml, "format"); format_attr.has_value())
162 {
163 metadata.format_explicit = true;
164 const std::string value = toLowerCopy(*format_attr);
165 if (value == "linear")
166 {
167 metadata.format = AxisGainFormat::Linear;
168 }
169 else if (value == "dbi")
170 {
171 metadata.format = AxisGainFormat::DBi;
172 }
173 else
174 {
175 throw std::runtime_error("Unsupported format '" + *format_attr + "' on <" + axis_name + "> axis.");
176 }
177 }
178
179 if (const auto symmetry_attr = XmlElement::getOptionalAttribute(axisXml, "symmetry"); symmetry_attr.has_value())
180 {
181 metadata.symmetry_explicit = true;
182 const std::string value = toLowerCopy(*symmetry_attr);
183 if (value == "mirrored")
184 {
186 }
187 else if (value == "none")
188 {
189 metadata.symmetry = antenna::XmlAntenna::AxisSymmetry::None;
190 }
191 else
192 {
193 throw std::runtime_error("Unsupported symmetry '" + *symmetry_attr + "' on <" + axis_name + "> axis.");
194 }
195 }
196
197 return metadata;
198 }
199
200 /**
201 * @brief Compute the sinc function.
202 *
203 * @param theta The angle for which to compute the sinc function.
204 * @return The value of the sinc function at the given angle theta.
205 */
206 RealType sinc(const RealType theta) noexcept
207 {
208 if (std::abs(theta) < EPSILON)
209 {
210 return 1.0;
211 }
212 return std::sin(theta) / theta;
213 }
214
215 /**
216 * @brief Compute the Bessel function of the first kind.
217 *
218 * @param x The value for which to compute the Bessel function.
219 * @return The value of the Bessel function of the first kind at the given value x.
220 */
221 RealType j1C(const RealType x) noexcept { return x == 0 ? 1.0 : core::besselJ1(x) / x; }
222
223 void loadAntennaGainSample(const interp::InterpSet* set, const AxisMetadata& metadata, const std::string& axis_name,
224 const XmlElement& sample, AxisSampleStats& stats)
225 {
226 XmlElement const angle_element = sample.childElement("angle", 0);
227 XmlElement const gain_element = sample.childElement("gain", 0);
228 if (!angle_element.isValid() || !gain_element.isValid())
229 {
230 if (sample.name() == "gainsample")
231 {
232 throw std::runtime_error("Each <gainsample> in <" + axis_name + "> must contain <angle> and <gain>.");
233 }
234 return;
235 }
236
237 const RealType raw_angle = parseRealValue(angle_element.getText(), "<" + axis_name + "> sample angle");
238 const RealType raw_gain = parseRealValue(gain_element.getText(), "<" + axis_name + "> sample gain");
239 const RealType angle = metadata.unit == AxisUnit::Degrees ? raw_angle * (PI / 180.0) : raw_angle;
240 const RealType gain = metadata.format == AxisGainFormat::DBi ? std::pow(10.0, raw_gain / 10.0) : raw_gain;
241
242 if (!std::isfinite(angle))
243 {
244 throw std::runtime_error("Converted <" + axis_name + "> sample angle is non-finite.");
245 }
246 if (!std::isfinite(gain))
247 {
248 throw std::runtime_error("Converted <" + axis_name + "> sample gain is non-finite.");
249 }
250 if (gain < 0.0)
251 {
252 throw std::runtime_error("Converted <" + axis_name + "> sample gain must be non-negative.");
253 }
254
255 set->insertSample(angle, gain);
256 stats.min_angle = std::min(stats.min_angle, angle);
257 stats.max_angle = std::max(stats.max_angle, angle);
258 stats.max_gain = std::max(stats.max_gain, gain);
259 ++stats.sample_count;
260 }
261
262 void validateAxisSymmetry(const AxisMetadata& metadata, AxisLoadResult& result, const std::string& axis_name,
263 const RealType min_angle, const RealType max_angle)
264 {
265 const bool spans_zero = min_angle < 0.0 && max_angle > 0.0;
266
267 if (metadata.symmetry_explicit)
268 {
269 if (result.symmetry == antenna::XmlAntenna::AxisSymmetry::Mirrored && min_angle < 0.0)
270 {
271 throw std::runtime_error("XML antenna <" + axis_name +
272 "> axis uses symmetry='mirrored' but defines negative sample angles.");
273 }
274 if (result.symmetry == antenna::XmlAntenna::AxisSymmetry::None && !spans_zero)
275 {
276 throw std::runtime_error(
277 "XML antenna <" + axis_name +
278 "> axis uses symmetry='none' but does not span both negative and positive angles.");
279 }
280 return;
281 }
282
283 if (min_angle < 0.0)
284 {
285 if (!spans_zero)
286 {
287 throw std::runtime_error("XML antenna <" + axis_name +
288 "> axis contains negative sample angles but does not span both sides of zero "
289 "for direct signed-angle lookup.");
290 }
292 }
293 }
294
295 /**
296 * @brief Load antenna gain axis data from an XML element.
297 *
298 * @param set The interpolation set to store the gain axis data.
299 * @param axisXml The XML element containing the gain axis data.
300 */
301 AxisLoadResult loadAntennaGainAxis(const interp::InterpSet* set, const XmlElement& axisXml)
302 {
303 if (!axisXml.isValid())
304 {
305 throw std::runtime_error("XML antenna pattern is missing a required gain axis.");
306 }
307
308 const AxisMetadata metadata = parseAxisMetadata(axisXml);
309 const std::string axis_name(axisXml.name());
310 XmlElement sample = axisXml.childElement("gainsample");
311 if (!sample.isValid())
312 {
313 throw std::runtime_error("XML antenna <" + axis_name + "> axis must contain at least one <gainsample>.");
314 }
315
316 AxisSampleStats stats;
317
318 while (sample.isValid())
319 {
320 loadAntennaGainSample(set, metadata, axis_name, sample, stats);
321 sample = XmlElement(sample.getNode()->next);
322 }
323
324 AxisLoadResult result;
325 result.max_gain = stats.max_gain;
326 result.symmetry = metadata.symmetry;
327 result.sample_count = stats.sample_count;
328 validateAxisSymmetry(metadata, result, axis_name, stats.min_angle, stats.max_angle);
329
330 const char* unit_source = metadata.unit_explicit ? "explicit" : "legacy default";
331 const char* format_source = metadata.format_explicit ? "explicit" : "legacy default";
332 const char* symmetry_source = metadata.symmetry_explicit
333 ? "explicit"
334 : (result.symmetry == antenna::XmlAntenna::AxisSymmetry::None ? "auto-detected" : "legacy default");
335
336 LOG(Level::INFO,
337 "XML antenna axis '{}' using unit='{}' ({}) format='{}' ({}) symmetry='{}' ({}) with {} samples.",
338 axis_name, axisUnitName(metadata.unit), unit_source, axisFormatName(metadata.format), format_source,
339 axisSymmetryName(result.symmetry), symmetry_source, result.sample_count);
340
341 return result;
342 }
343}
344
345namespace antenna
346{
347 void Antenna::setEfficiencyFactor(const RealType loss) noexcept
348 {
349 if (loss > 1)
350 {
351 LOG(Level::INFO, "Using greater than unity antenna efficiency.");
352 }
353 _loss_factor = loss;
354 }
355
356 RealType Antenna::getAngle(const SVec3& angle, const SVec3& refangle) noexcept
357 {
358 SVec3 normangle(angle);
359 normangle.length = 1;
360 return std::acos(dotProduct(Vec3(normangle), Vec3(refangle)));
361 }
362
363 RealType Gaussian::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const noexcept
364 {
365 const SVec3 a = angle - refangle;
366 return std::exp(-a.azimuth * a.azimuth * _azscale) * std::exp(-a.elevation * a.elevation * _elscale);
367 }
368
369 RealType Sinc::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const noexcept
370 {
371 const RealType theta = getAngle(angle, refangle);
372 const RealType sinc_val = sinc(_beta * theta);
373 const RealType gain_pattern = std::pow(std::abs(sinc_val), _gamma);
374 return _alpha * gain_pattern * getEfficiencyFactor();
375 }
376
377 RealType SquareHorn::getGain(const SVec3& angle, const SVec3& refangle, const RealType wavelength) const noexcept
378 {
379 const RealType ge = 4 * PI * std::pow(_dimension, 2) / std::pow(wavelength, 2);
380 const RealType x = PI * _dimension * std::sin(getAngle(angle, refangle)) / wavelength;
381 return ge * std::pow(sinc(x), 2) * getEfficiencyFactor();
382 }
383
384 RealType Parabolic::getGain(const SVec3& angle, const SVec3& refangle, const RealType wavelength) const noexcept
385 {
386 const RealType ge = std::pow(PI * _diameter / wavelength, 2);
387 const RealType x = PI * _diameter * std::sin(getAngle(angle, refangle)) / wavelength;
388 return ge * std::pow(2 * j1C(x), 2) * getEfficiencyFactor();
389 }
390
391 std::optional<RealType> XmlAntenna::lookupAxisGain(const interp::InterpSet* set, const RealType angle,
392 const AxisSymmetry symmetry) const noexcept
393 {
394 if (symmetry == AxisSymmetry::Mirrored)
395 {
396 return set->getValueAt(std::abs(angle));
397 }
398 return set->getValueAt(angle);
399 }
400
401 RealType XmlAntenna::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const
402 {
403 const SVec3 delta_angle = angle - refangle;
404
405 const std::optional<RealType> azi_value =
406 lookupAxisGain(_azi_samples.get(), delta_angle.azimuth, _azi_symmetry);
407
408 if (const std::optional<RealType> elev_value =
409 lookupAxisGain(_elev_samples.get(), delta_angle.elevation, _elev_symmetry);
410 azi_value && elev_value)
411 {
412 return *azi_value * *elev_value * _max_gain * getEfficiencyFactor();
413 }
414
415 LOG(Level::FATAL, "Could not get antenna gain value");
416 throw std::runtime_error("Could not get antenna gain value");
417 }
418
419 void XmlAntenna::loadAntennaDescription(const std::string_view filename)
420 {
421 _filename = filename;
422 XmlDocument doc;
423 if (!doc.loadFile(std::string(filename)))
424 {
425 LOG(Level::FATAL, "Could not load antenna description {}", filename.data());
426 throw std::runtime_error("Could not load antenna description");
427 }
428 (void)doc.validateWithDtd(antenna_pattern_dtd);
429 (void)doc.validateWithXsd(antenna_pattern_xsd);
430
431 const XmlElement root(doc.getRootElement());
432 const AxisLoadResult elev_result = loadAntennaGainAxis(_elev_samples.get(), root.childElement("elevation", 0));
433 const AxisLoadResult azi_result = loadAntennaGainAxis(_azi_samples.get(), root.childElement("azimuth", 0));
434
435 _elev_symmetry = elev_result.symmetry;
436 _azi_symmetry = azi_result.symmetry;
437
438 _max_gain = std::max(azi_result.max_gain, elev_result.max_gain);
439 if (!std::isfinite(_max_gain) || _max_gain <= 0.0)
440 {
441 throw std::runtime_error("XML antenna pattern peak linear gain must be greater than zero.");
442 }
443 _elev_samples->divide(_max_gain);
444 _azi_samples->divide(_max_gain);
445 }
446
447 RealType H5Antenna::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const
448 {
449 constexpr RealType two_pi = 2.0 * PI;
450
451 const SVec3& pattern_angle = angle - refangle;
452
453 const double ex1 = (pattern_angle.azimuth + PI) / two_pi;
454 const double ey1 = (pattern_angle.elevation + PI) / two_pi;
455
456 const auto calc_grid_point = [](const double value, const std::size_t size)
457 {
458 const double grid_size = static_cast<double>(size - 1);
459 const double x1 = std::floor(value * grid_size) / grid_size;
460 const double x2 = std::min(x1 + 1.0 / static_cast<double>(size), 1.0);
461 return std::pair{x1, x2};
462 };
463
464 const std::size_t size_azi = _pattern.size();
465 const std::size_t size_elev = _pattern[0].size();
466
467 LOG(logging::Level::TRACE, "Size of pattern: {} x {}", size_azi, size_elev);
468
469 const auto [x1, x2] = calc_grid_point(ex1, size_azi);
470 const auto [y1, y2] = calc_grid_point(ey1, size_elev);
471
472 const double t = (ex1 - x1) / (x2 - x1);
473 const double u = (ey1 - y1) / (y2 - y1);
474
475 const auto calc_array_index = [](const double value, const std::size_t size)
476 { return std::min(static_cast<std::size_t>(std::floor(value * static_cast<double>(size))), size - 1); };
477
478 const std::size_t arr_x = calc_array_index(x1, size_azi);
479 const std::size_t arr_y = calc_array_index(y1, size_elev);
480
481 const RealType interp = (1.0 - t) * (1.0 - u) * _pattern[arr_x][arr_y] +
482 t * (1.0 - u) * _pattern[(arr_x + 1) % size_azi][arr_y] +
483 t * u * _pattern[(arr_x + 1) % size_azi][(arr_y + 1) % size_elev] +
484 (1.0 - t) * u * _pattern[arr_x][(arr_y + 1) % size_elev];
485
486 return interp * getEfficiencyFactor();
487 }
488}
Header file defining various types of antennas and their gain patterns.
Class for managing XML documents.
XmlElement getRootElement() const
Get the root element of the document.
bool loadFile(std::string_view filename)
Load an XML file into the document.
bool validateWithDtd(std::span< const unsigned char > dtdData) const
Validate the document using a DTD.
bool validateWithXsd(std::span< const unsigned char > xsdData) const
Validate the document using an XSD schema.
Class representing a node in an XML document.
XmlElement childElement(const std::string_view name="", const unsigned index=0) const noexcept
Retrieve a child element by name and index.
static std::optional< std::string > getOptionalAttribute(const XmlElement &element, const std::string_view name)
Get the value of an optional attribute.
bool isValid() const noexcept
Check if the XML element is valid.
xmlNodePtr getNode() const noexcept
Get the underlying XML node pointer.
std::string name() const
Get the name of the XML element.
std::string getText() const
Get the text content of the XML element.
static RealType getAngle(const math::SVec3 &angle, const math::SVec3 &refangle) noexcept
Computes the angle between the input and reference angles.
void setEfficiencyFactor(RealType loss) noexcept
Sets the efficiency factor of the antenna.
RealType getEfficiencyFactor() const noexcept
Retrieves the efficiency factor of the antenna.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const noexcept override
Computes the gain of the Gaussian antenna.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType) const override
Computes the gain of the antenna based on the input angle and reference angle.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const noexcept override
Computes the gain of the parabolic antenna.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const noexcept override
Computes the gain of the sinc antenna based on the input parameters.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const noexcept override
Computes the gain of the square horn antenna.
RealType getGain(const math::SVec3 &angle, const math::SVec3 &refangle, RealType wavelength) const override
Computes the gain of the antenna based on the input angle and reference angle.
AxisSymmetry
Symmetry mode for one-dimensional XML antenna gain axes.
@ Mirrored
Mirror positive-axis samples onto negative angles.
@ None
Use the axis samples exactly as provided.
Wrapper class for managing interpolation sets using smart pointers.
std::optional< T > getValueAt(T x) const noexcept
Retrieves the interpolated value at a given point.
void insertSample(T x, T y) const noexcept
Inserts a sample point into the interpolation set.
A class representing a vector in spherical coordinates.
RealType elevation
The elevation angle of the vector.
RealType azimuth
The azimuth angle of the vector.
RealType length
The length of the vector.
A class representing a vector in rectangular coordinates.
Global configuration file for the project.
double RealType
Type for real numbers.
Definition config.h:27
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Classes and operations for 3D geometry.
Wrapper for managing XML documents and elements using libxml2.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
RealType besselJ1(const RealType x) noexcept
Computes the Bessel function of the first kind (order 1) for a given value.
@ TRACE
Trace level for detailed debugging information.
Utility functions for mathematical and system operations.
RealType a