📐

単位ベクトルの小数点誤差問題と許容適正値の求め方

2023/09/30に公開

問題の再現手順

ベクトル (x, y) を正規化する。

x = 10.374225137998149
y = 12.24930872223954
length_squared = x * x + y * y      # => 257.67011138660644
length = Math.sqrt(length_squared)  # => 16.052106135538928
x = x / length                      # => 0.6462843598467054
y = y / length                      # => 0.7630966689860031

正規化されているか確認する。

length_squared = x * x + y * y  # => 0.9999999999999993

正しく正規化されていればここで 1.0 になるはずだが少しずれている。かといってロジックが間違っているわけでもないので多少の誤差は許容しないといけない。誤差を許容するためには 1.0 との差分の絶対値が Float::EPSILON 以下かを判定すればよい。このときのために Float::EPSILON は用意されているのだから、もちろん結果は true になるはずだろう。

diff = (length_squared - 1.0).abs  # => 6.661338147750939e-16
diff <= Float::EPSILON             # => false

なんで?

"%.16f" % Float::EPSILON  # => "0.0000000000000002"
"%.16f" % diff            # => "0.0000000000000007"

誤差の許容値よりも誤差の方が大きくなっている。これは誤差を含んだ状態で length_squared を計算したことで誤差が広がって Float::EPSILON との比較に意味がなくなってしまったのだろうか?

他のライブラリを見てみる。

Rust 製ベクトルライブラリ glam

1e-4 がハードコーディングされていた。

https://github.com/bitshifter/glam-rs/blob/5fd61e965f777dc0e3c3d353e9007e5a7c957304/src/f64/dvec3.rs#L469-L476

1e-4 は 0.0001 のことなので相当緩い。コメントには 1e-6 とあり、コードとの乖離に迷走している様子が窺える。そしてもっとも気になるのが TODO のコメントで、そこには epsilon を使うべきとある。

想像だが、作者は f64::EPSILON[1] を使って書いたあと、今回のようになぜか誤差の方が大きくなることに気づいて、やむをえず一時的に緩い閾値を直感でハードコーディングし、TODO を書き残したのかもしれない。

Godot Engine

https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/vector2.cpp#L67-L70

https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/math_defs.h#L51-L57

こちらは規定の EPSILON を使っても正しく判定できないことを把握しているようで、コメントを要約すると「通常は 0.001 でよいが正確に判定するなら 0.00001 を使う」とのこと。

適正値は?

Godot Engine を参考に 0.00001 とした。

UNIT_EPSILON = 0.00001
diff <= UNIT_EPSILON  # => true

とりあえず同じにしておいただけで、どのような根拠でその値が出てきたのかがわからないので、腑に落ちないところでもあるが、正規化判定は主にデバッグに使うのでこれでいいことにしておく。

脚注
  1. Ruby の Float::EPSILON と同じ値 ↩︎

Discussion