Skip to content

Commit 920c8f8

Browse files
committed
Add rounding direction investigation and false positive discovery
Extended float32 precision investigation based on the hypothesis that rounding direction differences could cause false negatives. Discovered false positive case instead and updated comprehensive analysis. New Test Scripts: - test_rounding_direction.py: Tests for rounding direction mismatches - Attempts to find values that round in opposite directions - Tests guaranteed opposite rounding at float32 precision boundaries - Explores midpoint rounding and nextafter gaps - test_different_sources.py: Tests values from different sources - Computed vs literal values - Accumulated computation effects - Separate float32 array creation - Binary representation differences - **FOUND FALSE POSITIVE**: accumulated_f64 < direct_f64 (no overlap) but both round to 100.0 in float32 (false overlap detected) - test_false_negative_found.py: Systematic false negative search - Accumulated overlap loss scenarios - Different accumulation rates - NextAfter gap exploration - String roundtrip testing Key Findings: 1. False Positive Discovered: - Float64: sum(0.1 for _ in range(1000)) ≈ 99.999 < 100.0 (no overlap) - Float32: both values round to 100.0 (overlap detected) - This is the INVERSE of the reported issue 2. False Negative Still Not Reproduced: - Closed interval semantics (<=) handles all boundary cases correctly - Consistent rounding ensures internal consistency - All touching boxes are properly detected 3. Theoretical False Negative Scenarios Identified: - Different computation paths leading to same logical value - Compiler optimization with intermediate precision - FPU settings and register precision differences - Data pipeline inconsistencies - Platform-dependent floating point behavior Updated FLOAT32_PRECISION_ANALYSIS.md: - Added rounding direction investigation section - Documented false positive discovery - Expanded theoretical risk analysis - Added compiler/FPU/platform considerations - Updated recommendations and next steps Conclusion: While false negatives could not be reproduced synthetically, false positives were found. The reported issue likely requires specific real-world data, compiler settings, or platform configurations to manifest. Requesting concrete failing examples from reporter for further investigation.
1 parent b114f12 commit 920c8f8

File tree

4 files changed

+896
-6
lines changed

4 files changed

+896
-6
lines changed

FLOAT32_PRECISION_ANALYSIS.md

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,24 +135,128 @@ float32の精度限界:
135135

136136
## 検証スクリプト
137137

138-
以下の3つのテストスクリプトを作成しました
138+
以下のテストスクリプトを作成しました
139139

140140
1. `test_float32_overlap_issue.py`: 基本的な偽陰性テスト
141141
2. `test_float32_refined.py`: より厳密な精度境界テスト
142142
3. `test_float32_extreme.py`: 極端なエッジケーステスト
143+
4. `test_rounding_direction.py`: 丸め方向の不一致テスト
144+
5. `test_different_sources.py`: 異なるソースからの値の丸めテスト
145+
6. `test_false_negative_found.py`: 偽陰性の系統的探索
143146

144147
実行方法:
145148
```bash
146149
python test_float32_overlap_issue.py
147150
python test_float32_refined.py
148151
python test_float32_extreme.py
152+
python test_rounding_direction.py
153+
python test_different_sources.py
154+
python test_false_negative_found.py
149155
```
150156

157+
## 更新: 丸め方向の調査
158+
159+
「丸める方向が違う場合」という指摘に基づき、追加調査を実施しました。
160+
161+
### 重要な発見:偽陽性の検出
162+
163+
`test_different_sources.py``test_accumulated_computation()`**偽陽性**を検出:
164+
165+
```python
166+
# 累積計算による丸め誤差
167+
accumulated_f64 = sum(0.1 for _ in range(1000)) # ≈ 99.999...
168+
direct_f64 = 100.0
169+
170+
# Float64: accumulated < direct (重ならない)
171+
# Float32: 両方が 100.0 に丸まる (重なる!)
172+
173+
Result:
174+
- Float64 tree: 0 pairs (正しい)
175+
- Float32 tree: 1 pair (偽陽性!)
176+
```
177+
178+
これは報告された問題(偽陰性)の逆パターンです。float32の丸め込みにより、本来重ならないボックスが重なっていると誤判定されています。
179+
180+
### 偽陰性が再現できない理由の分析
181+
182+
1. **閉区間セマンティクス**: `<=` 比較により、境界で接触するボックスは常に交差と判定される
183+
2. **一貫した丸め込み**: 同じfloat64値は常に同じfloat32値に丸められる
184+
3. **内部一貫性**: すべての計算がfloat32で行われるため、比較は一貫している
185+
186+
### 理論的な偽陰性発生シナリオ
187+
188+
報告された問題が発生する可能性のある状況:
189+
190+
1. **異なる計算パス**:
191+
```
192+
Box A: 外部計算 -> float64 -> float32 (ツリー構築時)
193+
Box B: 別の計算 -> float64 -> float32 (ツリー構築時)
194+
```
195+
計算履歴の違いにより、本来重なるべき値が異なるfloat32表現になる可能性
196+
197+
2. **コンパイラの最適化による中間精度**:
198+
- C++コンパイラがfloat64中間精度を使用する場合がある
199+
- `-ffloat-store``-fexcess-precision=standard`フラグの影響
200+
- 最適化レベル(-O2, -O3)による挙動の違い
201+
202+
3. **FPU設定とレジスタ精度**:
203+
- x87 FPUの80bit拡張精度レジスタの影響
204+
- SSE/AVX命令セットの使用有無
205+
- 丸めモードの設定(RN, RZ, RP, RM)
206+
207+
4. **データパイプラインの不整合**:
208+
```
209+
Box A: ファイル読込 -> 文字列 -> float64 -> float32
210+
Box B: 直接計算 -> float64 -> float32
211+
```
212+
これらが微妙に異なる値になる可能性
213+
214+
5. **プラットフォーム依存の挙動**:
215+
- Windows vs Linux vs macOS での浮動小数点演算の違い
216+
- ハードウェアアーキテクチャ(x86, ARM)の違い
217+
151218
## 結論
152219

153-
1. **コードレベルでの脆弱性確認**: float32入力時の補正メカニズムの欠如を確認
154-
2. **実際の偽陰性の再現**: テストケースでは再現できず
155-
3. **理論的なリスク**: 特定の条件下(大きな座標値、微小な重なり)で偽陰性が発生する可能性あり
156-
4. **推奨事項**: 高精度が必要な場合はfloat64入力を使用すること
220+
1. **コードレベルでの脆弱性確認**:
221+
- float32入力時の補正メカニズムの欠如を確認
222+
- `include/prtree/core/prtree.h:157` で明示的に補正なしと記載
223+
224+
2. **偽陰性の再現**:
225+
- 合成テストケースでは再現できず
226+
- すべての境界接触ケースで正しく検出される
227+
228+
3. **偽陽性の発見**:
229+
- 累積計算による丸め誤差で偽陽性を確認
230+
- float64では重ならないがfloat32では重なる
231+
232+
4. **理論的なリスク**:
233+
- 偽陰性: 異なる計算パス、コンパイラ最適化、FPU設定の違い
234+
- 偽陽性: 累積計算による丸め誤差
235+
236+
5. **推奨事項**:
237+
- **重要**: 高精度が必要な場合はfloat64入力を使用
238+
- 累積計算を避け、直接計算を使用
239+
- データパイプラインの一貫性を確保
240+
- クリティカルな用途では float64 + 補正メカニズムに依存
241+
242+
## 次のステップ
243+
244+
この問題を完全に検証・解決するには:
245+
246+
1. **報告者からの情報収集**:
247+
- 具体的な失敗するデータセット(座標値)
248+
- 発生環境の詳細(OS、コンパイラ、最適化フラグ)
249+
- データの生成方法や処理パイプライン
250+
- ビルド時のCMakeオプション
251+
252+
2. **再現テスト**:
253+
- 実際のデータでの検証
254+
- 異なるプラットフォームでのテスト
255+
- コンパイラオプションを変えてのビルド
256+
257+
3. **潜在的な修正**:
258+
- float32入力でも `idx2exact` を保持するオプション追加
259+
- 精度警告システムの実装
260+
- ドキュメントでの精度制限の明示
157261

158-
この問題を完全に解決するには、報告者から具体的なデータセットや再現手順の提供が必要です
262+
これらの情報があれば、問題を再現し、適切な修正を行うことができます

test_different_sources.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test for rounding issues when values come from different sources.
4+
5+
The hypothesis: When Box A's max and Box B's min are computed or stored
6+
independently, float32 rounding can create gaps that don't exist in float64.
7+
"""
8+
import numpy as np
9+
from python_prtree import PRTree2D
10+
11+
12+
def test_computed_vs_literal():
13+
"""
14+
Test when coordinates come from computations vs literals.
15+
16+
A computed value might round differently than a literal value
17+
due to intermediate precision.
18+
"""
19+
print("\n=== Test: Computed vs Literal Values ===\n")
20+
21+
# Computed value (with intermediate float64 precision)
22+
computed_f64 = np.float64(1.0) / np.float64(3.0) * np.float64(300.0) # = 100.0
23+
24+
# Literal value
25+
literal_f64 = np.float64(100.0)
26+
27+
print(f"Computed (f64): {computed_f64:.20f}")
28+
print(f"Literal (f64): {literal_f64:.20f}")
29+
print(f"Equal in f64: {computed_f64 == literal_f64}")
30+
31+
computed_f32 = np.float32(computed_f64)
32+
literal_f32 = np.float32(literal_f64)
33+
34+
print(f"\nComputed (f32): {computed_f32:.20f}")
35+
print(f"Literal (f32): {literal_f32:.20f}")
36+
print(f"Equal in f32: {computed_f32 == literal_f32}")
37+
38+
# Create boxes
39+
boxes = np.array([
40+
[0.0, 0.0, computed_f64, 100.0], # Ends at computed value
41+
[literal_f64, 0.0, 200.0, 100.0], # Starts at literal value
42+
], dtype=np.float64)
43+
44+
boxes_f32 = boxes.astype(np.float32)
45+
46+
print(f"\nOverlap (f64): {boxes[0,2] >= boxes[1,0]}")
47+
print(f"Overlap (f32): {boxes_f32[0,2] >= boxes_f32[1,0]}")
48+
49+
idx = np.array([0, 1], dtype=np.int64)
50+
51+
tree_f64 = PRTree2D(idx, boxes)
52+
pairs_f64 = tree_f64.query_intersections()
53+
54+
tree_f32 = PRTree2D(idx, boxes_f32)
55+
pairs_f32 = tree_f32.query_intersections()
56+
57+
print(f"\nFloat64 tree: {len(pairs_f64)} pairs")
58+
print(f"Float32 tree: {len(pairs_f32)} pairs")
59+
60+
if len(pairs_f64) != len(pairs_f32):
61+
print("\n❌ FALSE NEGATIVE!")
62+
return True
63+
return False
64+
65+
66+
def test_accumulated_computation():
67+
"""
68+
Test when values are accumulated through multiple operations.
69+
"""
70+
print("\n=== Test: Accumulated Computation ===\n")
71+
72+
# Create a value through accumulation
73+
accumulated_f64 = np.float64(0.0)
74+
step = np.float64(0.1)
75+
for i in range(1000):
76+
accumulated_f64 += step
77+
78+
# Direct value
79+
direct_f64 = np.float64(100.0)
80+
81+
print(f"Accumulated (f64): {accumulated_f64:.20f}")
82+
print(f"Direct (f64): {direct_f64:.20f}")
83+
print(f"Difference: {abs(accumulated_f64 - direct_f64):.20e}")
84+
85+
accumulated_f32 = np.float32(accumulated_f64)
86+
direct_f32 = np.float32(direct_f64)
87+
88+
print(f"\nAccumulated (f32): {accumulated_f32:.20f}")
89+
print(f"Direct (f32): {direct_f32:.20f}")
90+
print(f"Equal in f32: {accumulated_f32 == direct_f32}")
91+
92+
# Create boxes with these values
93+
boxes_f64 = np.array([
94+
[0.0, 0.0, accumulated_f64, 100.0],
95+
[direct_f64, 0.0, 200.0, 100.0],
96+
], dtype=np.float64)
97+
98+
boxes_f32 = boxes_f64.astype(np.float32)
99+
100+
print(f"\nOverlap (f64): {boxes_f64[0,2] >= boxes_f64[1,0]}")
101+
print(f"Overlap (f32): {boxes_f32[0,2] >= boxes_f32[1,0]}")
102+
103+
idx = np.array([0, 1], dtype=np.int64)
104+
105+
tree_f64 = PRTree2D(idx, boxes_f64)
106+
pairs_f64 = tree_f64.query_intersections()
107+
108+
tree_f32 = PRTree2D(idx, boxes_f32)
109+
pairs_f32 = tree_f32.query_intersections()
110+
111+
print(f"\nFloat64 tree: {len(pairs_f64)} pairs")
112+
print(f"Float32 tree: {len(pairs_f32)} pairs")
113+
114+
if len(pairs_f64) != len(pairs_f32):
115+
print("\n❌ FALSE NEGATIVE!")
116+
return True
117+
return False
118+
119+
120+
def test_separate_float32_arrays():
121+
"""
122+
Test when float32 values are created in separate arrays.
123+
124+
Key insight: If two float32 values are created independently,
125+
they might have different representations even if they should be equal.
126+
"""
127+
print("\n=== Test: Separate Float32 Arrays ===\n")
128+
129+
# Create a problematic float64 value
130+
problematic = np.float64(100.0) + np.float64(1e-7)
131+
132+
print(f"Problematic value (f64): {problematic:.20f}")
133+
134+
# Create first array with this value as max
135+
array1_f32 = np.array([0.0, 0.0, problematic, 100.0], dtype=np.float32)
136+
137+
# Create second array with this value as min
138+
array2_f32 = np.array([problematic, 0.0, 200.0, 100.0], dtype=np.float32)
139+
140+
print(f"\nArray1[2] (max): {array1_f32[2]:.20f}")
141+
print(f"Array2[0] (min): {array2_f32[0]:.20f}")
142+
print(f"Equal: {array1_f32[2] == array2_f32[0]}")
143+
print(f"Overlap: {array1_f32[2] >= array2_f32[0]}")
144+
145+
# Combine into boxes
146+
boxes_f32 = np.vstack([array1_f32.reshape(1, -1), array2_f32.reshape(1, -1)])
147+
148+
print(f"\nCombined boxes (f32):\n{boxes_f32}")
149+
150+
idx = np.array([0, 1], dtype=np.int64)
151+
152+
tree = PRTree2D(idx, boxes_f32)
153+
pairs = tree.query_intersections()
154+
155+
print(f"\nIntersections found: {len(pairs)}")
156+
print(f"Pairs: {pairs}")
157+
158+
# Expected: should find intersection since they touch
159+
if len(pairs) == 0:
160+
print("\n❌ FALSE NEGATIVE: Touching boxes not detected!")
161+
return True
162+
163+
return False
164+
165+
166+
def test_binary_representation():
167+
"""
168+
Test values that have identical decimal representation but different binary.
169+
"""
170+
print("\n=== Test: Binary Representation ===\n")
171+
172+
# Create a value that cannot be exactly represented in binary
173+
decimal_val = 0.1
174+
175+
# In float64
176+
val_f64 = np.float64(decimal_val)
177+
print(f"0.1 in float64: {val_f64:.60f}")
178+
print(f"Hex: {val_f64.hex()}")
179+
180+
# In float32
181+
val_f32 = np.float32(decimal_val)
182+
print(f"\n0.1 in float32: {val_f32:.60f}")
183+
print(f"Hex: {val_f32.hex()}")
184+
185+
# Scale up
186+
scale = 1000
187+
scaled_f64 = val_f64 * scale
188+
scaled_f32 = val_f32 * scale
189+
190+
print(f"\nScaled (f64): {scaled_f64:.60f}")
191+
print(f"Scaled (f32): {scaled_f32:.60f}")
192+
193+
# Now use these as coordinates
194+
boxes_f64 = np.array([
195+
[0.0, 0.0, scaled_f64, 100.0],
196+
[scaled_f64, 0.0, 200.0, 100.0],
197+
], dtype=np.float64)
198+
199+
# Create float32 version two ways:
200+
# 1. Convert from float64
201+
boxes_f32_converted = boxes_f64.astype(np.float32)
202+
203+
# 2. Create directly with float32
204+
boxes_f32_direct = np.array([
205+
[0.0, 0.0, val_f32 * scale, 100.0],
206+
[val_f32 * scale, 0.0, 200.0, 100.0],
207+
], dtype=np.float32)
208+
209+
print(f"\nBoxes f32 (converted):\n{boxes_f32_converted}")
210+
print(f"\nBoxes f32 (direct):\n{boxes_f32_direct}")
211+
print(f"\nAre they equal? {np.array_equal(boxes_f32_converted, boxes_f32_direct)}")
212+
213+
idx = np.array([0, 1], dtype=np.int64)
214+
215+
tree_f64 = PRTree2D(idx, boxes_f64)
216+
pairs_f64 = tree_f64.query_intersections()
217+
218+
tree_f32_conv = PRTree2D(idx, boxes_f32_converted)
219+
pairs_f32_conv = tree_f32_conv.query_intersections()
220+
221+
tree_f32_dir = PRTree2D(idx, boxes_f32_direct)
222+
pairs_f32_dir = tree_f32_dir.query_intersections()
223+
224+
print(f"\nFloat64 tree: {len(pairs_f64)} pairs")
225+
print(f"Float32 (converted): {len(pairs_f32_conv)} pairs")
226+
print(f"Float32 (direct): {len(pairs_f32_dir)} pairs")
227+
228+
if len(pairs_f64) != len(pairs_f32_conv) or len(pairs_f64) != len(pairs_f32_dir):
229+
print("\n❌ FALSE NEGATIVE!")
230+
return True
231+
232+
return False
233+
234+
235+
if __name__ == "__main__":
236+
print("=" * 70)
237+
print("Testing Different Sources of Rounding")
238+
print("=" * 70)
239+
240+
issue_found = False
241+
242+
issue_found |= test_computed_vs_literal()
243+
issue_found |= test_accumulated_computation()
244+
issue_found |= test_separate_float32_arrays()
245+
issue_found |= test_binary_representation()
246+
247+
print("\n" + "=" * 70)
248+
if issue_found:
249+
print("❌ FALSE NEGATIVE CONFIRMED!")
250+
else:
251+
print("⚠️ No false negatives in these tests")
252+
print("=" * 70)

0 commit comments

Comments
 (0)