Как сравнивать координаты с плавающей точкой с учетом особенностей float в разных языках
Что проверяют
Понимаете ли вы, почему 0.1 + 0.2 != 0.3 и почему одного абсолютного epsilon мало. Следом спрашивают, как выбрать допуск для координат и чем отличаются функции сравнения в Python, NumPy и других языках.
Как отвечать
Почти все языки используют IEEE 754: double хранит около 15 значащих десятичных цифр, float32 около 7. Сравнивать нужно с допуском, и он должен учитывать масштаб:
- абсолютный:
abs(a-b) <= atol. Годится около нуля и для координат в известных единицах; - относительный:
abs(a-b) <= rtol * max(abs(a), abs(b)). Работает на любом масштабе, но ломается у нуля; - комбинированный:
math.isclose(a, b, rel_tol=1e-9, abs_tol=...). По умолчаниюabs_tol=0.0, поэтому ненулевое число с нулём без явногоabs_tolне совпадёт никогда; - NumPy: у
np.iscloseиnp.allcloseпо умолчаниюrtol=1e-05,atol=1e-08, и формула несимметрична, допуск считается от второго аргумента. Для тестов удобенnp.testing.assert_allclose; - ULP: сравнение по числу представимых значений между числами, в C++ через
std::nextafter, в Java черезMath.ulp.
Для координат допуск задают в единицах задачи: для широты и долготы 1e-7 градуса это около сантиметра. Точки сравнивают по расстоянию, а не по каждой оси отдельно. NaN не равен ничему, даже себе, а у np.isclose по умолчанию equal_nan=False.
Языки различаются в основном типами: в Java есть BigDecimal, в C# decimal, в Python decimal.Decimal, а обычные числа JavaScript всегда double. В Spark FloatType занимает 4 байта, и неудачная схема теряет точность уже при загрузке.
Где спотыкаются
Берут фиксированный 1e-9 для всего подряд. Для координат в метрах в проекции Меркатора это строже машинной точности, а для малых величин слишком грубо.
Сравнивают с нулём через math.isclose без abs_tol и не понимают, почему тест падает.
Округляют через round() и сравнивают результат. Два близких числа по разные стороны границы округления разойдутся, а Python ещё и округляет половину к чётному.