FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
rotation_warning_utils.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
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <format>
13#include <limits>
14#include <numbers>
15#include <numeric>
16#include <ranges>
17#include <string>
18
19#include "core/logging.h"
20
22{
23 namespace
24 {
25 thread_local std::vector<std::string> captured_warnings;
26
27 constexpr RealType kEpsilon = 1e-9;
28 constexpr RealType kMatchTolerance = 1e-3;
29 constexpr RealType kIrrationalLookingTolerance = 5e-3;
30
31 constexpr std::array<RealType, 12> common_degree_angles = {0.0, 30.0, 45.0, 60.0, 90.0, 120.0,
32 135.0, 150.0, 180.0, 225.0, 270.0, 360.0};
33
34 constexpr std::array<RealType, 9> common_radian_angles = {0.0,
35 std::numbers::pi_v<RealType> / 6.0,
36 std::numbers::pi_v<RealType> / 4.0,
37 std::numbers::pi_v<RealType> / 3.0,
38 std::numbers::pi_v<RealType> / 2.0,
39 2.0 * std::numbers::pi_v<RealType> / 3.0,
40 3.0 * std::numbers::pi_v<RealType> / 4.0,
41 5.0 * std::numbers::pi_v<RealType> / 6.0,
42 std::numbers::pi_v<RealType>};
43
44 [[nodiscard]] bool is_close(const RealType a, const RealType b,
45 const RealType tolerance = kMatchTolerance) noexcept
46 {
47 return std::abs(a - b) <= tolerance;
48 }
49
50 [[nodiscard]] bool is_near_integer(const RealType value) noexcept
51 {
52 return is_close(value, std::round(value), 1e-6);
53 }
54
55 [[nodiscard]] bool is_near_tenth(const RealType value) noexcept
56 {
57 return is_close(value * 10.0, std::round(value * 10.0), kIrrationalLookingTolerance * 10.0);
58 }
59
60 [[nodiscard]] bool matches_common_angle(const RealType abs_value,
61 const std::ranges::input_range auto& candidates) noexcept
62 {
63 for (const RealType candidate : candidates)
64 {
65 if (is_close(abs_value, candidate))
66 {
67 return true;
68 }
69 }
70 return false;
71 }
72
73 [[nodiscard]] bool matches_simple_pi_ratio(const RealType abs_value) noexcept
74 {
75 if (abs_value <= kEpsilon)
76 {
77 return false;
78 }
79
80 const RealType pi = std::numbers::pi_v<RealType>;
81 for (int numerator = 1; numerator <= 12; ++numerator)
82 {
83 for (int denominator = 1; denominator <= 12; ++denominator)
84 {
85 if (std::gcd(numerator, denominator) != 1)
86 {
87 continue;
88 }
89
90 const RealType candidate =
91 pi * static_cast<RealType>(numerator) / static_cast<RealType>(denominator);
92 if (is_close(abs_value, candidate))
93 {
94 return true;
95 }
96 }
97 }
98 return false;
99 }
100
101 [[nodiscard]] RealType best_clean_trig_distance(const RealType radians) noexcept
102 {
103 constexpr std::array<RealType, 5> clean_values = {0.0, 0.5, std::numbers::sqrt2_v<RealType> / 2.0,
104 std::numbers::sqrt3_v<RealType> / 2.0, 1.0};
105
106 RealType best = std::numeric_limits<RealType>::max();
107 for (const RealType value : {std::abs(std::sin(radians)), std::abs(std::cos(radians))})
108 {
109 for (const RealType clean : clean_values)
110 {
111 best = std::min(best, std::abs(value - clean));
112 }
113 }
114 return best;
115 }
116
117 [[nodiscard]] std::string unit_token(const params::RotationAngleUnit unit)
118 {
119 return std::string(params::rotationAngleUnitToken(unit));
120 }
121
122 [[nodiscard]] std::string confidence_token(const Confidence confidence)
123 {
124 switch (confidence)
125 {
126 case Confidence::High:
127 return "high";
129 return "medium";
130 case Confidence::Low:
131 return "low";
132 case Confidence::None:
133 default:
134 return "none";
135 }
136 }
137 }
138
139 InferenceResult infer_unit_from_value(const RealType value, const ValueKind kind) noexcept
140 {
141 const RealType abs_value = std::abs(value);
142 const RealType pi = std::numbers::pi_v<RealType>;
143 const RealType two_pi = 2.0 * pi;
144
145 InferenceResult result;
146 if (abs_value <= kEpsilon)
147 {
148 return result;
149 }
150
151 if (abs_value > 360.0)
152 {
153 result.degree_score += 8;
154 }
155 else if (abs_value > 180.0)
156 {
157 result.degree_score += 7;
158 }
159 else if (abs_value > two_pi)
160 {
161 result.degree_score += abs_value < 10.0 ? 3 : 6;
162 }
163 else if (abs_value > pi)
164 {
165 result.degree_score += 2;
166 }
167
168 const bool degree_famous = matches_common_angle(abs_value, common_degree_angles);
169 const bool radian_famous = matches_common_angle(abs_value, common_radian_angles);
170 if (degree_famous && !radian_famous)
171 {
172 result.degree_score += 3;
173 }
174 else if (radian_famous && !degree_famous)
175 {
176 result.radian_score += 3;
177 }
178
179 if (is_near_integer(abs_value))
180 {
181 const RealType rounded = std::round(abs_value);
182 if ((std::fmod(rounded, 45.0) == 0.0) || (std::fmod(rounded, 30.0) == 0.0) ||
183 (std::fmod(rounded, 15.0) == 0.0) || (std::fmod(rounded, 10.0) == 0.0) ||
184 (std::fmod(rounded, 5.0) == 0.0))
185 {
186 result.degree_score += 2;
187 }
188 else
189 {
190 result.degree_score += 1;
191 }
192 }
193 else if ((abs_value <= two_pi + 1.0) && !is_near_tenth(abs_value))
194 {
195 result.radian_score += 1;
196 }
197
198 if (matches_simple_pi_ratio(abs_value))
199 {
200 result.radian_score += 3;
201 }
202
203 if ((kind == ValueKind::Angle) && (abs_value <= two_pi + 0.5) &&
204 (std::abs(result.degree_score - result.radian_score) <= 1))
205 {
206 const RealType degree_distance = best_clean_trig_distance(value * pi / 180.0);
207 const RealType radian_distance = best_clean_trig_distance(value);
208 if (degree_distance + 1e-4 < radian_distance)
209 {
210 ++result.degree_score;
211 }
212 else if (radian_distance + 1e-4 < degree_distance)
213 {
214 ++result.radian_score;
215 }
216 }
217
218 if (result.degree_score == result.radian_score)
219 {
221 return result;
222 }
223
226 const int lead = std::abs(result.degree_score - result.radian_score);
227 const int max_score = std::max(result.degree_score, result.radian_score);
228
229 if ((lead >= 5) || ((max_score >= 7) && (lead >= 3)))
230 {
232 }
233 else if ((lead >= 3) || ((max_score >= 5) && (lead >= 2)))
234 {
236 }
237 else if ((lead >= 2) || (max_score >= 4))
238 {
240 }
241
242 return result;
243 }
244
245 bool should_warn(const Confidence confidence, const WarningSensitivity sensitivity) noexcept
246 {
247 switch (sensitivity)
248 {
250 return confidence == Confidence::High;
252 return (confidence == Confidence::Medium) || (confidence == Confidence::High);
254 return confidence != Confidence::None;
255 default:
256 return false;
257 }
258 }
259
260 void clear_captured_warnings() noexcept { captured_warnings.clear(); }
261
262 std::vector<std::string> take_captured_warnings()
263 {
264 std::vector<std::string> warnings = std::move(captured_warnings);
265 captured_warnings.clear();
266 return warnings;
267 }
268
270 const ValueKind kind, const std::string_view source,
271 const std::string_view owner, const std::string_view field)
272 {
273 const InferenceResult inference = infer_unit_from_value(value, kind);
274 if ((inference.inferred_unit == declared_unit) || !should_warn(inference.confidence, kWarningSensitivity))
275 {
276 return;
277 }
278
279 const std::string message =
280 std::format("{} rotation {} '{}' looks like {} but '{}' was declared (confidence: {}, value: {}). "
281 "Change rotationangleunit or convert existing values.",
282 source, owner, field, unit_token(inference.inferred_unit), unit_token(declared_unit),
283 confidence_token(inference.confidence), value);
284
285 if (std::ranges::find(captured_warnings, message) == captured_warnings.end())
286 {
287 captured_warnings.push_back(message);
288 }
289
290 LOG(logging::Level::WARNING, "{}", message);
291 }
292}
double RealType
Type for real numbers.
Definition config.h:27
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
@ WARNING
Warning level for potentially harmful situations.
RotationAngleUnit
Defines the units used at external rotation-path boundaries.
Definition parameters.h:41
@ Radians
Compass azimuth and elevation expressed in radians.
@ Degrees
Compass azimuth and elevation expressed in degrees.
constexpr std::string_view rotationAngleUnitToken(const RotationAngleUnit unit) noexcept
Definition parameters.h:294
std::vector< std::string > take_captured_warnings()
void maybe_warn_about_rotation_value(const RealType value, const params::RotationAngleUnit declared_unit, const ValueKind kind, const std::string_view source, const std::string_view owner, const std::string_view field)
bool should_warn(const Confidence confidence, const WarningSensitivity sensitivity) noexcept
constexpr WarningSensitivity kWarningSensitivity
InferenceResult infer_unit_from_value(const RealType value, const ValueKind kind) noexcept