Вычисление расстояния между двумя точками в полярных координатах с использованием чисел с плавающей запятой

Когда координаты двух точек на плоскости представлены в полярной форме как r1, a1 и r2, a2, где r1, r2, a1, a2 — числа с плавающей запятой, и цель состоит в том, чтобы вычислить расстояние между двумя точками в виде числа с плавающей запятой, может возникнуть соблазн использовать следующую математическую формулу:

D = sqrt(r1*r1 + r2*r2 - 2*r1*r2*cos(a1-a2))

Эту формулу можно найти в этом и нескольких других ответах на StackOverflow. Ссылка на источник, указанный в связанном ответе, мертва, но вы найдете эту формулу во многих математических ресурсах, например этого веб-сайта.

Как формула с плавающей запятой, эта формула имеет ряд нежелательных свойств, перечисленных ниже в порядке уменьшения серьезности. Короче говоря, мой вопрос: какая формула лучше для вычисления расстояния D в условиях, указанных выше?

A/ sqrt применяется к отрицательному числу, когда r1 = r2 и r1*r1 субнормальны

Когда r1*r1 ниже нормы, а r1 = r2, округление r1*r1 отличается от округления 2*r1*r2. На одном полюсе этого спектра контрпримеров r1*r1 равно нулю, а 2*r1*r2 отлично от нуля. С другой стороны, r1*r1 является субнормальным и округляется несколько резко вниз, а 2*r1*r2 является нормальным и округляется менее резко. В любом случае выражение внутри sqrt оказывается отрицательным, а результатом формулы с плавающей запятой для D является NaN.

Примеры значений для двойной точности:

  double r1 = sqrt(DBL_MIN * DBL_EPSILON) * sqrt(0.45);
  double r2 = r1;

Запустите его в Compiler Explorer.

B/ sqrt применяется к отрицательному числу, когда r1 близко к r2, а отличается

Когда r1 и r2 очень близки, расстояние между математическими произведениями r1*r1 и r1*r2 очень близко к расстоянию между математическими произведениями r1*r2 и r2*r2. Когда это общее расстояние соответствует небольшому нечетному числу половинных ULP, может возникнуть ситуация, когда умножение с плавающей запятой r1*r1 округляется в меньшую сторону, r1*r2 округляется в большую сторону и r2*r2 снова округляется в меньшую сторону. В этих условиях, опять же, часто встречающаяся формула берет квадратный корень из отрицательного числа.

Примеры значений для двойной точности:

  double r1 = 0x1.1c71c71c71d3fp0;
  double r2 = 0x1.1c71c71c71dcbp0;

Запустите его в Compiler Explorer.

C/ Когда r1 близко к r2, вычитание увеличивает приближения, которые имеют место при умножении

На самом деле это более мягкий симптом тех же основных причин проблем A/ и B/. Когда r1 очень близко к r2, возникает явление, известное как катастрофическая отмена. Относительная точность вычитания ужасна (например, если a1 = a2 и r1 близки к r2, округление при умножении может сделать результат вычитания равным 0,0, хотя оптимальный ответ, fabs(r1 - r2), был представим и бесконечно более точен в относительных единицах. Примечание. что абсолютная точность результата порядка ULP r1 и r2, и это все еще может быть хорошо.

D/Переполнение, когда величина r1 или r2 слишком велика

Если переполняется только r1*r1 или r2*r2, результат вычисляется как +inf, что может быть не лучшим представимым приближением математического расстояния.

Если r1*r2 переполняется, то результатом вычитания является NaN, и поэтому расстояние вычисляется как NaN.


Проблемы A/ и B/ делают бессмысленным результат, которого не должно быть, и могут быть решены путем вычисления dq > 0 ? sqrt(dq) : 0 вместо dq. Для входных данных, вызвавших их, это изменение делает ответ 0.0 вместо этого. Этот результат имеет бесконечную относительную ошибку, как и результаты для других входных данных, потому что это не решает проблему C/.

Проблема D/ может быть решена путем масштабирования, если программист ожидает, что вычисление будет использоваться в условиях, которые его запускают. Если на то пошло, проблема A/ также может быть решена масштабированием, но это не решит проблему B/.

Вполне вероятно, что полное решение, которое решает все проблемы A-D, потребует больше вычислений. Может существовать несколько золотых точек, которые решают только некоторые проблемы или решают проблему C/ более или менее тщательно, вычисляя расстояния с точностью до 10 ULP, или 3, или 1. Любое решение, которое улучшается по сравнению с исходной точкой, является достойный ответа.

Гийом Мелькион уже указывал за пределами сайта, что приведенная ниже формула эквивалентна оригиналу в математических терминах и, очевидно, избегает вопросов A/ и B/, поскольку аргумент квадратного корня представляет собой сумму неотрицательных членов:

D = sqrt((r1-r2)*(r1-r2) + 2*r1*r2*(1 - cos(a2-a1)))

В этом решении катастрофическая отмена происходит в 1 - cos(a2-a1), поэтому некоторые аспекты проблемы С/ остаются (хотя расчет по этой формуле оптимален для a1=a2 и r1, близких к r2, так как тогда r1-r2 и cos(a2-a1) точны). Ситуация с проблемой D/ улучшилась, но остались случаи, когда результаты представлялись в виде конечного значения, а формула вычисляла +inf.


person Pascal Cuoq    schedule 09.09.2019    source источник
comment
Даже если это звучит глупо, как насчет cartesian_distance(pol2cart(p1),pol2cart(p2))? если декартово расстояние использует hypot() для защиты от переполнения / недополнения, а pol2cart использует sincos(), это не намного дороже, разве это не устраняет большую часть проблем?   -  person aka.nice    schedule 09.09.2019
comment
@aka.nice Это очень интересный подход. Чуть было не указал на аналогию со стандартной функцией hypot для декартова расстояния, но вопрос уже достаточно длинный.   -  person Pascal Cuoq    schedule 09.09.2019
comment
Чтобы избежать промежуточного переполнения и потери значимости, я бы предложил масштабировать r1 и r2 по одной и той же степени основания (предполагая, что основание 2, но основание 10 может работать аналогично), чтобы после масштабирования большее из двух находилось между 0.5 и 1.0 в абсолютная величина. Тогда должно быть легко показать, что вычисление с масштабированными значениями защищено как от потери значимости, так и от переполнения, а потеря значимости или переполнение произойдет только в результате окончательного обратного масштабирования. Конечно, сначала вам понадобится специальный регистр r1 = 0.0 и/или r2 = 0.0. Кстати, вам нужно/хотите разрешить отрицательные r1 или r2?   -  person Mark Dickinson    schedule 09.09.2019
comment
@MarkDickinson Я должен был сказать, что r1 и r2 неотрицательны. Я все еще не решаюсь наложить ограничения на a1 и a2.   -  person Pascal Cuoq    schedule 09.09.2019
comment
Я думаю, что знак r1 и r2 не имеет большого значения; случай, когда оба отрицательны, тривиально прост (просто игнорируйте знаки), и если они имеют противоположные знаки, то в конечном итоге вы захотите вычислить 1 + cos(a2 - a1) вместо 1 - cos(a2-a1), и снова это выражается как удвоение простого квадрата. Настоящая проблема заключается в том, чтобы точно вычислить что-то вроде 2 * sin((a1 - a2)/2) по всем возможным a1 и a2, и действительно ограничения на величины a1 и a2 очень помогли бы в этом.   -  person Mark Dickinson    schedule 09.09.2019
comment
... и решение состоит не в том, чтобы вычислять sin((a1 - a2)/2), как написано, а в том, чтобы расширить его как sin(a1/2)cos(a2/2) - cos(a1/2)sin(a2/2), эффективно используя способность вашей libm уменьшать большие значения по модулю 2*pi (при условии, что она дает точные результаты для cos(large) и sin(large), и, следовательно, есть такая способность). Было бы все еще трудно получить хорошую точность ulp в случае, когда (1) r1 очень близко (возможно, даже равно) к r2 и (2) a1 очень близко к a2, но только после сокращения по модулю 2*пи.   -  person Mark Dickinson    schedule 09.09.2019
comment
@MarkDickinson Если вы собираетесь опубликовать ответ с жесткой гарантией, выраженной в ULP, вам разрешено предположить, что входными данными проблемы являются r1 и r2 положительные значения с плавающей запятой и angdiff разница углов как значение с плавающей запятой. .   -  person Pascal Cuoq    schedule 10.09.2019
comment
Посмотрите, решит ли проблему добавление скобок в одном месте: ...2*(r1*r2)*cos...   -  person Rick James    schedule 20.11.2019
comment
@RickJames Это не так. Теперь r1*r2 переполняется или умножение этого на 2 делает. Вы можете попытаться быть милым и поместить (1 - cos(a2-a1)) в качестве первого фактора, поскольку он всегда меньше 1, но это само по себе не решает полностью проблему, которую вы получаете +inf, когда лучший результат может быть представлен в виде конечного числа с плавающей запятой. Обратите внимание, что проблема полностью решена в ответах.   -  person Pascal Cuoq    schedule 21.11.2019


Ответы (3)


Let b = (a1-a2)/2 
then using
cos( a1-a2) = 1 - 2*sin(b)*sin(b)
D = sqrt( (r1-r2)*(r1-r2) + 4*r1*r2*sin(b)*sin(b))

Это, по крайней мере, избавляет от квадратных корней отрицательных чисел, но все равно проблемы с переполнением.

Возможно

x = 2*sqrt(r1)*sqrt(r2)*sin(b)
D = hypot( r1-r2, x)

разрешит это?

person dmuir    schedule 09.09.2019
comment
Обратите внимание, что, хотя большинству пользователей, вероятно, все равно, a1-a2 также подвержен переполнению, потере точности и т. д. Например, если a1 близко к кратному 2pi больше, чем в несколько раз 2**53, а a2 приблизительно равно pi, вы должны вычислить расстояние, как если бы точки находились под одним и тем же углом, а не в противоположных направлениях вдоль оси x. . - person R.. GitHub STOP HELPING ICE; 09.09.2019
comment
Я не уверен, о какой «катастрофической отмене» вы говорите в предложении «Но есть еще катастрофическая отмена». Обратите внимание, что r1 и r2 являются входными данными задачи и что вопрос заключается в том, как вычислить наиболее точное расстояние при точном значении r1 и r2, а не в том, что произойдет, если r1 и r2 сами по себе являются приближениями других значений. Другими словами, если r1 и r2 близки по значению, r1-r2 не является случаем катастрофической отмены. Наоборот, точно! (лемма Стербенца) - person Pascal Cuoq; 10.09.2019
comment
В моем нынешнем понимании этот ответ содержит два алгоритма, которые обязательно следует использовать для вычисления расстояний в полярных координатах, в зависимости от того, боитесь ли вы переполнения или нет. Обратите внимание: если вы никогда не хотите, чтобы D вычислялось как +inf, когда конечное значение было бы лучшим ответом, x следует вычислять как sin(b)*2*sqrt(r1)*sqrt(r2) во втором алгоритме. - person Pascal Cuoq; 10.09.2019

Я не эксперт в такого рода численном анализе, но хотел бы отметить, что в наши дни существуют компьютерные программы, которые пытаются дать более «стабильную» версию вашей формулы в отношении проблем с плавающей запятой. Одной из лучших является так называемая система Herbie: https://herbie.uwplse.org/

У них есть веб-демонстрация, и когда я подключаю ваше уравнение, получается:

https://herbie.uwplse.org/demo/9c74ffee9d36a7f50669c498f99c86d1c0b4c837.f316bcef3de0492d34dcdbc4c663eb04d00305c4/graph.html

Вышеприведенная ссылка содержит массу дополнительной информации о переводе, который она рекомендует. На случай, если веб-ссылка исчезнет, ​​вот скриншот окончательной формулы, которую она предлагает:

введите здесь описание изображения

Вы также можете получить тот же результат в LaTeX или C. Он утверждает, что ошибка была уменьшена с 31,5 до 18,5; хотя я не уверен, что именно означают эти цифры. У них есть краткое руководство для начала работы: https://herbie.uwplse.org/doc/latest/tutorial.html

Надеюсь это поможет!

person alias    schedule 09.09.2019
comment
Это выглядит очень подозрительно для меня. Предположим, что мы игнорируем отрицательные значения r, в последнем предложении else предлагается использовать формулу r1 - 1 . (cos(a1 - a2) .r2) для всех случаев, где r1 > 5.81e-4. Это приближение может быть точным для крошечного r2, но оно будет сильно отличаться от желаемого результата, как только r2 станет совсем не крошечным - это совершенно другая (не эквивалентная математически) формула. Случай с r1 <= -0.0193 так же плох (он даже не зависит от a1 или a2!), но возможно, что OP не заботятся об отрицательных значениях r. - person Mark Dickinson; 09.09.2019
comment
Возможно, я неправильно использовал их интерфейс; хотя я действительно не проверял результаты на предмет правильности. Если вы считаете результат ошибочным, я уверен, что они будут рады услышать ваше мнение: github. com/uwplse/herbie/issues - person alias; 09.09.2019
comment
Я играл с Херби раньше и получал такие же плохие результаты; Я думаю, что сделал пару отчетов об ошибках, но я далеко не уверен, что это соответствует цели. Численные аналитики в ближайшее время не останутся без работы. - person Mark Dickinson; 09.09.2019

Расстояние останется прежним, если мы повернем всю фигуру.
Используя идею pol2cart, у нас могут быть некоторые варианты, такие как:

x1=r1 cos((a2-a1)/2),y1=-r1 sin((a2-a1)/2)
x2=r2 cos((a2-a1)/2),y2= r2 sin((a2-a1)/2)

dist = hypot((r2-r1) cos((a2-a1)/2),(r2+r1) sin((a2-a1)/2)))

Если углы не ограничены, а не уменьшены должным образом, тогда необходимо разработать формулы sin/cos, что сделает эту формулировку не столь полезной...
r2+r1 также может переполниться, и в этом случае мы могли бы применить простое масштабирование, такое как 2*hypot((r1/2+r2/2)...)

person aka.nice    schedule 09.09.2019
comment
За исключением вообще, вы не можете повернуть всю фигуру без катастрофических потерь. Разница a2-a1 может быть не представлена ​​какими-либо значащими битами. :-) - person R.. GitHub STOP HELPING ICE; 10.09.2019
comment
Что ж, я думаю, вы могли бы сделать это, сначала выполнив сокращение собственного аргумента, или atan(tan(...)), или что-то подобное. Но это уже намного превышает стоимость, которую вы хотите иметь для этой операции. - person R.. GitHub STOP HELPING ICE; 10.09.2019
comment
@R.. Я должен был сформулировать вопрос, сказав, что входными данными задачи были r1 и r2, радиальные координаты каждой точки в виде положительных чисел с плавающей запятой и angdiff разница между углами в виде числа с плавающей запятой. Это не приведет к потере общности любого существующего ответа (хотя это испортит обозначения). Я могу отредактировать вопрос и все ответы, чтобы будущие читатели не отвлекались на этот аспект. - person Pascal Cuoq; 10.09.2019