Я использую библиотеку с фиксированной точкой Энтони Уильямса, описанную в статье доктора Добба "" Оптимизация приложений с математической точки зрения с арифметикой с фиксированной точкой " рассчитать расстояние между двумя географическими точками, используя метод Rhumb Line.
Это работает достаточно хорошо, когда расстояние между точками значимо (больше нескольких километров), но очень мало на меньших расстояниях. В худшем случае, когда две точки равны или почти равны, результат составляет расстояние в 194 метра, тогда как мне нужна точность не менее 1 метра на расстояниях >= 1 метр.
По сравнению с реализацией с плавающей запятой с двойной точностью я обнаружил проблему с функцией fixed::sqrt()
, которая плохо работает при малых значениях:
x std::sqrt(x) fixed::sqrt(x) error
----------------------------------------------------
0 0 3.05176e-005 3.05176e-005
1e-005 0.00316228 0.00316334 1.06005e-006
2e-005 0.00447214 0.00447226 1.19752e-007
3e-005 0.00547723 0.0054779 6.72248e-007
4e-005 0.00632456 0.00632477 2.12746e-007
5e-005 0.00707107 0.0070715 4.27244e-007
6e-005 0.00774597 0.0077467 7.2978e-007
7e-005 0.0083666 0.00836658 1.54875e-008
8e-005 0.00894427 0.00894427 1.085e-009
Исправление результата для fixed::sqrt(0)
тривиально, рассматривая его как частный случай, но это не решит проблему для малых ненулевых расстояний, где ошибка начинается с 194 метров и с увеличением расстояния сходится к нулю. Вероятно, мне нужен, по крайней мере, порядок улучшения магнетика в точности до нуля.
Алгоритм fixed::sqrt()
кратко поясняется на странице 4 статьи, приведенной выше, но я изо всех сил стараюсь следовать ей, не говоря уже о том, можно ли ее улучшить. Код для функции воспроизводится ниже:
fixed fixed::sqrt() const
{
unsigned const max_shift=62;
uint64_t a_squared=1LL<<max_shift;
unsigned b_shift=(max_shift+fixed_resolution_shift)/2;
uint64_t a=1LL<<b_shift;
uint64_t x=m_nVal;
while(b_shift && a_squared>x)
{
a>>=1;
a_squared>>=2;
--b_shift;
}
uint64_t remainder=x-a_squared;
--b_shift;
while(remainder && b_shift)
{
uint64_t b_squared=1LL<<(2*b_shift-fixed_resolution_shift);
int const two_a_b_shift=b_shift+1-fixed_resolution_shift;
uint64_t two_a_b=(two_a_b_shift>0)?(a<<two_a_b_shift):(a>>-two_a_b_shift);
while(b_shift && remainder<(b_squared+two_a_b))
{
b_squared>>=2;
two_a_b>>=1;
--b_shift;
}
uint64_t const delta=b_squared+two_a_b;
if((2*remainder)>delta)
{
a+=(1LL<<b_shift);
remainder-=delta;
if(b_shift)
{
--b_shift;
}
}
}
return fixed(internal(),a);
}
Обратите внимание, что m_nVal
является внутренним значением представления фиксированной точки, это int64_t
, а в представлении используется Q36.28 format ( fixed_resolution_shift
= 28). Само представление имеет достаточную точность не менее 8 знаков после запятой, и поскольку доля экваториальной дуги хороша для расстояний около 0,14 м, поэтому ограничение не является представлением с фиксированной точкой.
Использование метода линии рубля является рекомендацией органа по стандартизации для этого приложения, поэтому его нельзя изменить, и в любом случае более точная функция с квадратным корнем, вероятно, потребуется в другом месте приложения или в будущих приложениях.
Вопрос: Возможно ли повысить точность алгоритма fixed::sqrt()
для небольших ненулевых значений, сохраняя при этом свою ограниченную и детерминированную сходимость?
Дополнительная информация Код проверки, используемый для создания таблицы выше:
#include <cmath>
#include <iostream>
#include "fixed.hpp"
int main()
{
double error = 1.0 ;
for( double x = 0.0; error > 1e-8; x += 1e-5 )
{
double fixed_root = sqrt(fixed(x)).as_double() ;
double std_root = std::sqrt(x) ;
error = std::fabs(fixed_root - std_root) ;
std::cout << x << '\t' << std_root << '\t' << fixed_root << '\t' << error << std::endl ;
}
}
Заключение В свете решения и анализа Джастина Пила и сравнения с алгоритмом в "Забытое искусство арифметики с фиксированной точкой" , я адаптировал последнее следующим образом:
fixed fixed::sqrt() const
{
uint64_t a = 0 ; // root accumulator
uint64_t remHi = 0 ; // high part of partial remainder
uint64_t remLo = m_nVal ; // low part of partial remainder
uint64_t testDiv ;
int count = 31 + (fixed_resolution_shift >> 1); // Loop counter
do
{
// get 2 bits of arg
remHi = (remHi << 2) | (remLo >> 62); remLo <<= 2 ;
// Get ready for the next bit in the root
a <<= 1;
// Test radical
testDiv = (a << 1) + 1;
if (remHi >= testDiv)
{
remHi -= testDiv;
a += 1;
}
} while (count-- != 0);
return fixed(internal(),a);
}
Несмотря на то, что это дает гораздо большую точность, улучшения, которые мне нужны, не должно быть достигнуто. Только формат Q36.28 обеспечивает точность, которая мне нужна, но невозможно выполнить sqrt() без потери нескольких бит точности. Однако некоторое боковое мышление обеспечивает лучшее решение. Мое приложение проверяет вычисленное расстояние на некоторое расстояние. Довольно очевидное решение в ретроспективе - проверить квадрат расстояния до квадрата предела!