FERS 1.0.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 <limits>
20#include <optional>
21#include <stdexcept>
22
23#include "antenna_pattern_dtd.h"
24#include "antenna_pattern_xsd.h"
25#include "core/config.h"
26#include "core/logging.h"
27#include "core/portable_utils.h"
28#include "math/geometry_ops.h"
30
31using logging::Level;
32using math::SVec3;
33using math::Vec3;
34
35namespace
36{
37 enum class AxisUnit
38 {
39 Radians,
40 Degrees,
41 };
42
43 enum class AxisGainFormat
44 {
45 Linear,
46 DBi,
47 };
48
49 struct AxisMetadata
50 {
51 AxisUnit unit{AxisUnit::Radians};
52 AxisGainFormat format{AxisGainFormat::Linear};
54 bool unit_explicit{};
55 bool format_explicit{};
56 bool symmetry_explicit{};
57 };
58
59 struct AxisLoadResult
60 {
61 RealType max_gain{};
63 std::size_t sample_count{};
64 };
65
66 std::string toLowerCopy(std::string value)
67 {
68 std::transform(value.begin(), value.end(), value.begin(),
69 [](const unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
70 return value;
71 }
72
73 RealType parseRealValue(const std::string_view text, const std::string_view context)
74 {
75 const std::string value(text);
76 size_t index = 0;
77 double parsed = 0.0;
78
79 try
80 {
81 parsed = std::stod(value, &index);
82 }
83 catch (const std::exception&)
84 {
85 throw std::runtime_error("Invalid numeric value for " + std::string(context) + ": '" + value + "'.");
86 }
87
88 while (index < value.size() && (std::isspace(static_cast<unsigned char>(value[index])) != 0))
89 {
90 ++index;
91 }
92
93 if (index != value.size())
94 {
95 throw std::runtime_error("Invalid numeric value for " + std::string(context) + ": '" + value + "'.");
96 }
97
98 if (!std::isfinite(parsed))
99 {
100 throw std::runtime_error("Non-finite numeric value for " + std::string(context) + ".");
101 }
102
103 return static_cast<RealType>(parsed);
104 }
105
106 const char* axisUnitName(const AxisUnit unit) noexcept { return unit == AxisUnit::Degrees ? "deg" : "rad"; }
107
108 const char* axisFormatName(const AxisGainFormat format) noexcept
109 {
110 return format == AxisGainFormat::DBi ? "dBi" : "linear";
111 }
112
113 const char* axisSymmetryName(const antenna::XmlAntenna::AxisSymmetry symmetry) noexcept
114 {
115 return symmetry == antenna::XmlAntenna::AxisSymmetry::None ? "none" : "mirrored";
116 }
117
118 AxisMetadata parseAxisMetadata(const XmlElement& axisXml)
119 {
120 AxisMetadata metadata;
121 const std::string axis_name(axisXml.name());
122
123 if (const auto unit_attr = XmlElement::getOptionalAttribute(axisXml, "unit"); unit_attr.has_value())
124 {
125 metadata.unit_explicit = true;
126 const std::string value = toLowerCopy(*unit_attr);
127 if (value == "rad")
128 {
129 metadata.unit = AxisUnit::Radians;
130 }
131 else if (value == "deg")
132 {
133 metadata.unit = AxisUnit::Degrees;
134 }
135 else
136 {
137 throw std::runtime_error("Unsupported unit '" + *unit_attr + "' on <" + axis_name + "> axis.");
138 }
139 }
140
141 if (const auto format_attr = XmlElement::getOptionalAttribute(axisXml, "format"); format_attr.has_value())
142 {
143 metadata.format_explicit = true;
144 const std::string value = toLowerCopy(*format_attr);
145 if (value == "linear")
146 {
147 metadata.format = AxisGainFormat::Linear;
148 }
149 else if (value == "dbi")
150 {
151 metadata.format = AxisGainFormat::DBi;
152 }
153 else
154 {
155 throw std::runtime_error("Unsupported format '" + *format_attr + "' on <" + axis_name + "> axis.");
156 }
157 }
158
159 if (const auto symmetry_attr = XmlElement::getOptionalAttribute(axisXml, "symmetry"); symmetry_attr.has_value())
160 {
161 metadata.symmetry_explicit = true;
162 const std::string value = toLowerCopy(*symmetry_attr);
163 if (value == "mirrored")
164 {
166 }
167 else if (value == "none")
168 {
169 metadata.symmetry = antenna::XmlAntenna::AxisSymmetry::None;
170 }
171 else
172 {
173 throw std::runtime_error("Unsupported symmetry '" + *symmetry_attr + "' on <" + axis_name + "> axis.");
174 }
175 }
176
177 return metadata;
178 }
179
180 /**
181 * @brief Compute the sinc function.
182 *
183 * @param theta The angle for which to compute the sinc function.
184 * @return The value of the sinc function at the given angle theta.
185 */
186 RealType sinc(const RealType theta) noexcept
187 {
188 if (std::abs(theta) < EPSILON)
189 {
190 return 1.0;
191 }
192 return std::sin(theta) / theta;
193 }
194
195 /**
196 * @brief Compute the Bessel function of the first kind.
197 *
198 * @param x The value for which to compute the Bessel function.
199 * @return The value of the Bessel function of the first kind at the given value x.
200 */
201 RealType j1C(const RealType x) noexcept { return x == 0 ? 1.0 : core::besselJ1(x) / x; }
202
203 /**
204 * @brief Load antenna gain axis data from an XML element.
205 *
206 * @param set The interpolation set to store the gain axis data.
207 * @param axisXml The XML element containing the gain axis data.
208 */
209 AxisLoadResult loadAntennaGainAxis(const interp::InterpSet* set, const XmlElement& axisXml)
210 {
211 if (!axisXml.isValid())
212 {
213 throw std::runtime_error("XML antenna pattern is missing a required gain axis.");
214 }
215
216 const AxisMetadata metadata = parseAxisMetadata(axisXml);
217 const std::string axis_name(axisXml.name());
218 XmlElement sample = axisXml.childElement("gainsample");
219 if (!sample.isValid())
220 {
221 throw std::runtime_error("XML antenna <" + axis_name + "> axis must contain at least one <gainsample>.");
222 }
223
224 RealType min_angle = std::numeric_limits<RealType>::max();
225 RealType max_angle = std::numeric_limits<RealType>::lowest();
226 RealType max_gain = 0.0;
227 std::size_t sample_count = 0;
228
229 while (sample.isValid())
230 {
231 XmlElement angle_element = sample.childElement("angle", 0);
232
233 if (XmlElement gain_element = sample.childElement("gain", 0);
234 angle_element.isValid() && gain_element.isValid())
235 {
236 const RealType raw_angle = parseRealValue(angle_element.getText(), "<" + axis_name + "> sample angle");
237 const RealType raw_gain = parseRealValue(gain_element.getText(), "<" + axis_name + "> sample gain");
238 const RealType angle = metadata.unit == AxisUnit::Degrees ? raw_angle * (PI / 180.0) : raw_angle;
239 const RealType gain =
240 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 min_angle = std::min(min_angle, angle);
257 max_angle = std::max(max_angle, angle);
258 max_gain = std::max(max_gain, gain);
259 ++sample_count;
260 }
261 else if (sample.name() == "gainsample")
262 {
263 throw std::runtime_error("Each <gainsample> in <" + axis_name + "> must contain <angle> and <gain>.");
264 }
265
266 sample = XmlElement(sample.getNode()->next);
267 }
268
269 AxisLoadResult result;
270 result.max_gain = max_gain;
271 result.symmetry = metadata.symmetry;
272 result.sample_count = sample_count;
273
274 if (metadata.symmetry_explicit)
275 {
276 if (result.symmetry == antenna::XmlAntenna::AxisSymmetry::Mirrored && min_angle < 0.0)
277 {
278 throw std::runtime_error("XML antenna <" + axis_name +
279 "> axis uses symmetry='mirrored' but defines negative sample angles.");
280 }
281 if (result.symmetry == antenna::XmlAntenna::AxisSymmetry::None && !(min_angle < 0.0 && max_angle > 0.0))
282 {
283 throw std::runtime_error(
284 "XML antenna <" + axis_name +
285 "> axis uses symmetry='none' but does not span both negative and positive angles.");
286 }
287 }
288 else if (min_angle < 0.0)
289 {
290 if (!(min_angle < 0.0 && max_angle > 0.0))
291 {
292 throw std::runtime_error("XML antenna <" + axis_name +
293 "> axis contains negative sample angles but does not span both sides of zero "
294 "for direct signed-angle lookup.");
295 }
297 }
298
299 const char* unit_source = metadata.unit_explicit ? "explicit" : "legacy default";
300 const char* format_source = metadata.format_explicit ? "explicit" : "legacy default";
301 const char* symmetry_source = metadata.symmetry_explicit
302 ? "explicit"
303 : (result.symmetry == antenna::XmlAntenna::AxisSymmetry::None ? "auto-detected" : "legacy default");
304
305 LOG(Level::INFO,
306 "XML antenna axis '{}' using unit='{}' ({}) format='{}' ({}) symmetry='{}' ({}) with {} samples.",
307 axis_name, axisUnitName(metadata.unit), unit_source, axisFormatName(metadata.format), format_source,
308 axisSymmetryName(result.symmetry), symmetry_source, result.sample_count);
309
310 return result;
311 }
312}
313
314namespace antenna
315{
316 void Antenna::setEfficiencyFactor(const RealType loss) noexcept
317 {
318 if (loss > 1)
319 {
320 LOG(Level::INFO, "Using greater than unity antenna efficiency.");
321 }
322 _loss_factor = loss;
323 }
324
325 RealType Antenna::getAngle(const SVec3& angle, const SVec3& refangle) noexcept
326 {
327 SVec3 normangle(angle);
328 normangle.length = 1;
329 return std::acos(dotProduct(Vec3(normangle), Vec3(refangle)));
330 }
331
332 RealType Gaussian::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const noexcept
333 {
334 const SVec3 a = angle - refangle;
335 return std::exp(-a.azimuth * a.azimuth * _azscale) * std::exp(-a.elevation * a.elevation * _elscale);
336 }
337
338 RealType Sinc::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const noexcept
339 {
340 const RealType theta = getAngle(angle, refangle);
341 const RealType sinc_val = sinc(_beta * theta);
342 const RealType gain_pattern = std::pow(std::abs(sinc_val), _gamma);
343 return _alpha * gain_pattern * getEfficiencyFactor();
344 }
345
346 RealType SquareHorn::getGain(const SVec3& angle, const SVec3& refangle, const RealType wavelength) const noexcept
347 {
348 const RealType ge = 4 * PI * std::pow(_dimension, 2) / std::pow(wavelength, 2);
349 const RealType x = PI * _dimension * std::sin(getAngle(angle, refangle)) / wavelength;
350 return ge * std::pow(sinc(x), 2) * getEfficiencyFactor();
351 }
352
353 RealType Parabolic::getGain(const SVec3& angle, const SVec3& refangle, const RealType wavelength) const noexcept
354 {
355 const RealType ge = std::pow(PI * _diameter / wavelength, 2);
356 const RealType x = PI * _diameter * std::sin(getAngle(angle, refangle)) / wavelength;
357 return ge * std::pow(2 * j1C(x), 2) * getEfficiencyFactor();
358 }
359
360 std::optional<RealType> XmlAntenna::lookupAxisGain(const interp::InterpSet* set, const RealType angle,
361 const AxisSymmetry symmetry) const noexcept
362 {
363 if (symmetry == AxisSymmetry::Mirrored)
364 {
365 return set->getValueAt(std::abs(angle));
366 }
367 return set->getValueAt(angle);
368 }
369
370 RealType XmlAntenna::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const
371 {
372 const SVec3 delta_angle = angle - refangle;
373
374 const std::optional<RealType> azi_value =
375 lookupAxisGain(_azi_samples.get(), delta_angle.azimuth, _azi_symmetry);
376
377 if (const std::optional<RealType> elev_value =
378 lookupAxisGain(_elev_samples.get(), delta_angle.elevation, _elev_symmetry);
379 azi_value && elev_value)
380 {
381 return *azi_value * *elev_value * _max_gain * getEfficiencyFactor();
382 }
383
384 LOG(Level::FATAL, "Could not get antenna gain value");
385 throw std::runtime_error("Could not get antenna gain value");
386 }
387
388 void XmlAntenna::loadAntennaDescription(const std::string_view filename)
389 {
390 _filename = filename;
391 XmlDocument doc;
392 if (!doc.loadFile(std::string(filename)))
393 {
394 LOG(Level::FATAL, "Could not load antenna description {}", filename.data());
395 throw std::runtime_error("Could not load antenna description");
396 }
397 (void)doc.validateWithDtd(antenna_pattern_dtd);
398 (void)doc.validateWithXsd(antenna_pattern_xsd);
399
400 const XmlElement root(doc.getRootElement());
401 const AxisLoadResult elev_result = loadAntennaGainAxis(_elev_samples.get(), root.childElement("elevation", 0));
402 const AxisLoadResult azi_result = loadAntennaGainAxis(_azi_samples.get(), root.childElement("azimuth", 0));
403
404 _elev_symmetry = elev_result.symmetry;
405 _azi_symmetry = azi_result.symmetry;
406
407 _max_gain = std::max(azi_result.max_gain, elev_result.max_gain);
408 if (!std::isfinite(_max_gain) || _max_gain <= 0.0)
409 {
410 throw std::runtime_error("XML antenna pattern peak linear gain must be greater than zero.");
411 }
412 _elev_samples->divide(_max_gain);
413 _azi_samples->divide(_max_gain);
414 }
415
416 RealType H5Antenna::getGain(const SVec3& angle, const SVec3& refangle, RealType /*wavelength*/) const
417 {
418 constexpr RealType two_pi = 2.0 * PI;
419
420 const SVec3& pattern_angle = angle - refangle;
421
422 const double ex1 = (pattern_angle.azimuth + PI) / two_pi;
423 const double ey1 = (pattern_angle.elevation + PI) / two_pi;
424
425 const auto calc_grid_point = [](const double value, const std::size_t size)
426 {
427 const double grid_size = static_cast<double>(size - 1);
428 const double x1 = std::floor(value * grid_size) / grid_size;
429 const double x2 = std::min(x1 + 1.0 / static_cast<double>(size), 1.0);
430 return std::pair{x1, x2};
431 };
432
433 const std::size_t size_azi = _pattern.size();
434 const std::size_t size_elev = _pattern[0].size();
435
436 LOG(logging::Level::TRACE, "Size of pattern: {} x {}", size_azi, size_elev);
437
438 const auto [x1, x2] = calc_grid_point(ex1, size_azi);
439 const auto [y1, y2] = calc_grid_point(ey1, size_elev);
440
441 const double t = (ex1 - x1) / (x2 - x1);
442 const double u = (ey1 - y1) / (y2 - y1);
443
444 const auto calc_array_index = [](const double value, const std::size_t size)
445 { return std::min(static_cast<std::size_t>(std::floor(value * static_cast<double>(size))), size - 1); };
446
447 const std::size_t arr_x = calc_array_index(x1, size_azi);
448 const std::size_t arr_y = calc_array_index(y1, size_elev);
449
450 const RealType interp = (1.0 - t) * (1.0 - u) * _pattern[arr_x][arr_y] +
451 t * (1.0 - u) * _pattern[(arr_x + 1) % size_azi][arr_y] +
452 t * u * _pattern[(arr_x + 1) % size_azi][(arr_y + 1) % size_elev] +
453 (1.0 - t) * u * _pattern[arr_x][(arr_y + 1) % size_elev];
454
455 return interp * getEfficiencyFactor();
456 }
457}
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.
std::string_view name() const noexcept
Get the name of the XML element.
bool isValid() const noexcept
Check if the XML element is valid.
xmlNodePtr getNode() const noexcept
Get the underlying XML node pointer.
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.
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.