単位ベクトルの小数点誤差問題と許容適正値の求め方
問題の再現手順
ベクトル (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 がハードコーディングされていた。
1e-4 は 0.0001 のことなので相当緩い。コメントには 1e-6 とあり、コードとの乖離に迷走している様子が窺える。そしてもっとも気になるのが TODO のコメントで、そこには epsilon を使うべきとある。
想像だが、作者は f64::EPSILON[1] を使って書いたあと、今回のようになぜか誤差の方が大きくなることに気づいて、やむをえず一時的に緩い閾値を直感でハードコーディングし、TODO を書き残したのかもしれない。
Godot Engine
こちらは規定の EPSILON を使っても正しく判定できないことを把握しているようで、コメントを要約すると「通常は 0.001 でよいが正確に判定するなら 0.00001 を使う」とのこと。
適正値は?
Godot Engine を参考に 0.00001 とした。
UNIT_EPSILON = 0.00001
diff <= UNIT_EPSILON # => true
とりあえず同じにしておいただけで、どのような根拠でその値が出てきたのかがわからないので、腑に落ちないところでもあるが、正規化判定は主にデバッグに使うのでこれでいいことにしておく。
-
Ruby の Float::EPSILON と同じ値 ↩︎
Discussion