float32への丸め込みで偽陰性(false negative)で重なりが検出されないという報告。
- すべてのバウンディングボックスは内部的に
Real型(=float)で保存される include/prtree/core/detail/bounding_box.h:17で定義:using Real = float;
float64入力の場合 (include/prtree/core/prtree.h:213-291):
// Constructor for float64 input (float32 tree + double refinement)
PRTree(const py::array_t<T> &idx, const py::array_t<double> &x)- 内部的にfloat32に変換してツリーを構築
- しかし、元のdouble精度の座標を
idx2exactに保存(line 274) - クエリ時に
refine_candidates()メソッド(line 805-831)でdouble精度で再チェック
float32入力の場合 (include/prtree/core/prtree.h:138-210):
// Constructor for float32 input (no refinement, pure float32 performance)
PRTree(const py::array_t<T> &idx, const py::array_t<float> &x)- float32のまま処理
idx2exactは空のまま(line 157のコメント: "idx2exact is NOT populated for float32 input")- 補正が行われない
include/prtree/core/detail/bounding_box.h:106-125:
bool operator()(const BB &target) const {
// ... (省略)
for (int i = 0; i < D; ++i) {
flags[i] = -minima[i] <= maxima[i]; // 閉区間セマンティクス
}
// ...
}- すべてfloat32で計算される
<=を使用(touching boxes are considered intersecting)
include/prtree/core/prtree.h:894-1040:
- line 963-965と993-996で、
idx2exactが空でない場合のみ補正を行う - float32入力の場合は補正がスキップされる(line 808-810)
-
丸め込みによる精度損失:
- float64で表現された微小な重なりがfloat32に変換される際に失われる可能性
- 特に大きな座標値(例: 10^7以上)では、float32のULP(Unit in Last Place)が大きくなる
-
補正メカニズムの非対称性:
- float64入力: float32ツリー + double補正
- float32入力: float32ツリーのみ(補正なし)
- これにより、同じデータでも入力型によって結果が異なる可能性
-
潜在的な偽陰性シナリオ:
- 2つのAABBが非常に小さな量で重なる
- その重なりがfloat32の精度限界以下
- float32に丸められた後、重ならなくなる
複数のテストケースを作成して検証を試みましたが、実際の偽陰性を再現することはできませんでした:
- test_float32_overlap_issue.py: 基本的な精度テスト
- test_float32_refined.py: より厳密な精度境界テスト
- test_float32_extreme.py: 極端なケース(ULP境界、負の座標、subnormal値など)
すべてのテストで偽陰性は検出されませんでした。理由として考えられるのは:
- 閉区間セマンティクス:
<=比較により、境界で接触するボックスは常に交差と判定される - 一貫した丸め込み: 両方のボックスが同じ精度(float32)で保存されるため、比較は一貫している
- テストケースの限界: 実際の報告された問題を再現する特定のデータセットが必要な可能性
報告された偽陰性の問題は、以下の条件で発生する可能性があります:
# float32でツリーを構築
boxes_f32 = np.array([[0.0, 0.0, 100.0, 100.0]], dtype=np.float32)
tree = PRTree2D(np.array([0]), boxes_f32)
# float64でクエリ(わずかに異なる境界値)
query_f64 = np.array([100.0 + epsilon, 0.0, 200.0, 100.0], dtype=np.float64)
result = tree.query(query_f64) # 偽陰性の可能性float32の精度限界:
- 100付近: ULP ≈ 7.6e-6
- 10,000付近: ULP ≈ 0.000977
- 1,000,000付近: ULP ≈ 0.0625
- 16,777,216 (2^24)付近: ULP = 2.0
大きな座標値では、微小な重なりが丸め込みで失われやすい。
複数の演算を経た座標値は、累積的な丸め込みエラーにより、 元の値から大きくずれる可能性がある。
-
float64入力の使用を推奨:
- float64入力を使用すれば、内部補正メカニズムにより高精度が保たれる
-
float32入力にも補正メカニズムを追加:
- float32で構築されたツリーでも、クエリ時にはdouble精度で再チェック
- ただし、元の精度情報が失われているため完全な補正は不可能
-
ドキュメントの改善:
- float32使用時の精度制限を明示的に文書化
- 高精度が必要な場合はfloat64の使用を推奨
-
精度警告の追加:
- 大きな座標値(>10^6)でfloat32を使用する場合、警告を表示
以下のテストスクリプトを作成しました:
test_float32_overlap_issue.py: 基本的な偽陰性テストtest_float32_refined.py: より厳密な精度境界テストtest_float32_extreme.py: 極端なエッジケーステストtest_rounding_direction.py: 丸め方向の不一致テストtest_different_sources.py: 異なるソースからの値の丸めテストtest_false_negative_found.py: 偽陰性の系統的探索
実行方法:
python test_float32_overlap_issue.py
python test_float32_refined.py
python test_float32_extreme.py
python test_rounding_direction.py
python test_different_sources.py
python test_false_negative_found.py「丸める方向が違う場合」という指摘に基づき、追加調査を実施しました。
test_different_sources.pyのtest_accumulated_computation()で偽陽性を検出:
# 累積計算による丸め誤差
accumulated_f64 = sum(0.1 for _ in range(1000)) # ≈ 99.999...
direct_f64 = 100.0
# Float64: accumulated < direct (重ならない)
# Float32: 両方が 100.0 に丸まる (重なる!)
Result:
- Float64 tree: 0 pairs (正しい)
- Float32 tree: 1 pair (偽陽性!)これは報告された問題(偽陰性)の逆パターンです。float32の丸め込みにより、本来重ならないボックスが重なっていると誤判定されています。
- 閉区間セマンティクス:
<=比較により、境界で接触するボックスは常に交差と判定される - 一貫した丸め込み: 同じfloat64値は常に同じfloat32値に丸められる
- 内部一貫性: すべての計算がfloat32で行われるため、比較は一貫している
報告された問題が発生する可能性のある状況:
-
異なる計算パス:
Box A: 外部計算 -> float64 -> float32 (ツリー構築時) Box B: 別の計算 -> float64 -> float32 (ツリー構築時)計算履歴の違いにより、本来重なるべき値が異なるfloat32表現になる可能性
-
コンパイラの最適化による中間精度:
- C++コンパイラがfloat64中間精度を使用する場合がある
-ffloat-storeや-fexcess-precision=standardフラグの影響- 最適化レベル(-O2, -O3)による挙動の違い
-
FPU設定とレジスタ精度:
- x87 FPUの80bit拡張精度レジスタの影響
- SSE/AVX命令セットの使用有無
- 丸めモードの設定(RN, RZ, RP, RM)
-
データパイプラインの不整合:
Box A: ファイル読込 -> 文字列 -> float64 -> float32 Box B: 直接計算 -> float64 -> float32これらが微妙に異なる値になる可能性
-
プラットフォーム依存の挙動:
- Windows vs Linux vs macOS での浮動小数点演算の違い
- ハードウェアアーキテクチャ(x86, ARM)の違い
-
コードレベルでの脆弱性確認:
- float32入力時の補正メカニズムの欠如を確認
include/prtree/core/prtree.h:157で明示的に補正なしと記載
-
偽陰性の再現:
- 合成テストケースでは再現できず
- すべての境界接触ケースで正しく検出される
-
偽陽性の発見:
- 累積計算による丸め誤差で偽陽性を確認
- float64では重ならないがfloat32では重なる
-
理論的なリスク:
- 偽陰性: 異なる計算パス、コンパイラ最適化、FPU設定の違い
- 偽陽性: 累積計算による丸め誤差
-
推奨事項:
- 重要: 高精度が必要な場合はfloat64入力を使用
- 累積計算を避け、直接計算を使用
- データパイプラインの一貫性を確保
- クリティカルな用途では float64 + 補正メカニズムに依存
この問題を完全に検証・解決するには:
-
報告者からの情報収集:
- 具体的な失敗するデータセット(座標値)
- 発生環境の詳細(OS、コンパイラ、最適化フラグ)
- データの生成方法や処理パイプライン
- ビルド時のCMakeオプション
-
再現テスト:
- 実際のデータでの検証
- 異なるプラットフォームでのテスト
- コンパイラオプションを変えてのビルド
-
潜在的な修正:
- float32入力でも
idx2exactを保持するオプション追加 - 精度警告システムの実装
- ドキュメントでの精度制限の明示
- float32入力でも
これらの情報があれば、問題を再現し、適切な修正を行うことができます。