24#include "antenna_pattern_dtd.h"
25#include "antenna_pattern_xsd.h"
39 enum class AxisUnit : std::uint8_t
46 enum class AxisGainFormat : std::uint8_t
55 AxisUnit unit{AxisUnit::Radians};
56 AxisGainFormat format{AxisGainFormat::Linear};
59 bool format_explicit{};
60 bool symmetry_explicit{};
69 std::size_t sample_count{};
72 struct AxisSampleStats
74 RealType min_angle{std::numeric_limits<RealType>::max()};
75 RealType max_angle{std::numeric_limits<RealType>::lowest()};
77 std::size_t sample_count{};
81 std::string toLowerCopy(std::string value)
83 std::ranges::transform(value, value.begin(),
84 [](
const unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
89 RealType parseRealValue(
const std::string_view text,
const std::string_view context)
91 const std::string value(text);
97 parsed = std::stod(value, &index);
99 catch (
const std::exception&)
101 throw std::runtime_error(
"Invalid numeric value for " + std::string(context) +
": '" + value +
"'.");
104 while (index < value.size() && (std::isspace(
static_cast<unsigned char>(value[index])) != 0))
109 if (index != value.size())
111 throw std::runtime_error(
"Invalid numeric value for " + std::string(context) +
": '" + value +
"'.");
114 if (!std::isfinite(parsed))
116 throw std::runtime_error(
"Non-finite numeric value for " + std::string(context) +
".");
119 return static_cast<RealType>(parsed);
123 const char* axisUnitName(
const AxisUnit unit)
noexcept {
return unit == AxisUnit::Degrees ?
"deg" :
"rad"; }
126 const char* axisFormatName(
const AxisGainFormat format)
noexcept
128 return format == AxisGainFormat::DBi ?
"dBi" :
"linear";
138 AxisMetadata parseAxisMetadata(
const XmlElement& axisXml)
140 AxisMetadata metadata;
141 const std::string axis_name(axisXml.
name());
145 metadata.unit_explicit =
true;
146 const std::string value = toLowerCopy(*unit_attr);
149 metadata.unit = AxisUnit::Radians;
151 else if (value ==
"deg")
153 metadata.unit = AxisUnit::Degrees;
157 throw std::runtime_error(
"Unsupported unit '" + *unit_attr +
"' on <" + axis_name +
"> axis.");
163 metadata.format_explicit =
true;
164 const std::string value = toLowerCopy(*format_attr);
165 if (value ==
"linear")
167 metadata.format = AxisGainFormat::Linear;
169 else if (value ==
"dbi")
171 metadata.format = AxisGainFormat::DBi;
175 throw std::runtime_error(
"Unsupported format '" + *format_attr +
"' on <" + axis_name +
"> axis.");
181 metadata.symmetry_explicit =
true;
182 const std::string value = toLowerCopy(*symmetry_attr);
183 if (value ==
"mirrored")
187 else if (value ==
"none")
193 throw std::runtime_error(
"Unsupported symmetry '" + *symmetry_attr +
"' on <" + axis_name +
"> axis.");
212 return std::sin(theta) / theta;
223 void loadAntennaGainSample(
const interp::InterpSet* set,
const AxisMetadata& metadata,
const std::string& axis_name,
224 const XmlElement& sample, AxisSampleStats& stats)
230 if (sample.
name() ==
"gainsample")
232 throw std::runtime_error(
"Each <gainsample> in <" + axis_name +
"> must contain <angle> and <gain>.");
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;
242 if (!std::isfinite(angle))
244 throw std::runtime_error(
"Converted <" + axis_name +
"> sample angle is non-finite.");
246 if (!std::isfinite(gain))
248 throw std::runtime_error(
"Converted <" + axis_name +
"> sample gain is non-finite.");
252 throw std::runtime_error(
"Converted <" + axis_name +
"> sample gain must be non-negative.");
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;
262 void validateAxisSymmetry(
const AxisMetadata& metadata, AxisLoadResult& result,
const std::string& axis_name,
265 const bool spans_zero = min_angle < 0.0 && max_angle > 0.0;
267 if (metadata.symmetry_explicit)
271 throw std::runtime_error(
"XML antenna <" + axis_name +
272 "> axis uses symmetry='mirrored' but defines negative sample angles.");
276 throw std::runtime_error(
277 "XML antenna <" + axis_name +
278 "> axis uses symmetry='none' but does not span both negative and positive angles.");
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.");
305 throw std::runtime_error(
"XML antenna pattern is missing a required gain axis.");
308 const AxisMetadata metadata = parseAxisMetadata(axisXml);
309 const std::string axis_name(axisXml.
name());
313 throw std::runtime_error(
"XML antenna <" + axis_name +
"> axis must contain at least one <gainsample>.");
316 AxisSampleStats stats;
320 loadAntennaGainSample(set, metadata, axis_name, sample, stats);
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);
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
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);
351 LOG(Level::INFO,
"Using greater than unity antenna efficiency.");
358 SVec3 normangle(angle);
360 return std::acos(dotProduct(
Vec3(normangle),
Vec3(refangle)));
365 const SVec3 a = angle - refangle;
366 return std::exp(-
a.azimuth *
a.azimuth * _azscale) * std::exp(-
a.elevation *
a.elevation * _elscale);
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();
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();
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();
392 const AxisSymmetry symmetry)
const noexcept
394 if (symmetry == AxisSymmetry::Mirrored)
403 const SVec3 delta_angle = angle - refangle;
405 const std::optional<RealType> azi_value =
406 lookupAxisGain(_azi_samples.get(), delta_angle.
azimuth, _azi_symmetry);
408 if (
const std::optional<RealType> elev_value =
409 lookupAxisGain(_elev_samples.get(), delta_angle.
elevation, _elev_symmetry);
410 azi_value && elev_value)
415 LOG(Level::FATAL,
"Could not get antenna gain value");
416 throw std::runtime_error(
"Could not get antenna gain value");
419 void XmlAntenna::loadAntennaDescription(
const std::string_view filename)
421 _filename = filename;
423 if (!doc.
loadFile(std::string(filename)))
425 LOG(Level::FATAL,
"Could not load antenna description {}", filename.data());
426 throw std::runtime_error(
"Could not load antenna description");
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));
435 _elev_symmetry = elev_result.symmetry;
436 _azi_symmetry = azi_result.symmetry;
438 _max_gain = std::max(azi_result.max_gain, elev_result.max_gain);
439 if (!std::isfinite(_max_gain) || _max_gain <= 0.0)
441 throw std::runtime_error(
"XML antenna pattern peak linear gain must be greater than zero.");
443 _elev_samples->divide(_max_gain);
444 _azi_samples->divide(_max_gain);
451 const SVec3& pattern_angle = angle - refangle;
453 const double ex1 = (pattern_angle.
azimuth +
PI) / two_pi;
454 const double ey1 = (pattern_angle.
elevation +
PI) / two_pi;
456 const auto calc_grid_point = [](
const double value,
const std::size_t size)
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};
464 const std::size_t size_azi = _pattern.size();
465 const std::size_t size_elev = _pattern[0].size();
469 const auto [x1, x2] = calc_grid_point(ex1, size_azi);
470 const auto [y1, y2] = calc_grid_point(ey1, size_elev);
472 const double t = (ex1 - x1) / (x2 - x1);
473 const double u = (ey1 - y1) / (y2 - y1);
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); };
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);
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];
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.
constexpr RealType EPSILON
Machine epsilon for real numbers.
constexpr RealType PI
Mathematical constant π (pi).
Classes and operations for 3D geometry.
Wrapper for managing XML documents and elements using libxml2.
Header file for the logging system.
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.