|
| 1 | +# Odd and prime-powered generalized-Bruun accuracy audit |
| 2 | + |
| 3 | +## Scope and verdict |
| 4 | + |
| 5 | +This report audits the odd-prime and prime-power path in `scratch_genbruun_exact.py`, especially the condition-1 cascade frame introduced for composite odd Bruun nodes. The result is mathematically correct as a DFT factorization, and the new cascade is the right structural fix for the previous Chebyshev inverse-coefficient blow-up. It is not a cheat: it is the direct complex block DFT written as two real planes. |
| 6 | + |
| 7 | +The metrology result is now much closer to the target after pairwise reductions and high-precision-generated twiddles. The current Python prototype is usually equal to or better than NumPy's mixed-radix/T-C style `rfft` on the sampled odd and prime-powered sizes, with normalized maximum-error ratios from **0.33x to 2.00x** NumPy's and most composite prime-power cases below NumPy. It is not a formal correctly rounded FFT, and it is not an unconditional win for every prime leaf, but the correct-twiddle path extracts the main available accuracy benefit from the odd/prime design. |
| 8 | + |
| 9 | +## Mathematical structure |
| 10 | + |
| 11 | +The top-level real transform factors `z^N - 1` by repeatedly removing an odd prime `p` from the active length `D = pM`. The DC branch is the block sum, while each conjugate branch is evaluated at `g = j / p` turns for `1 <= j <= (p - 1) / 2`. |
| 12 | + |
| 13 | +For a real block decomposition `R_i`, the condition-1 seed is |
| 14 | + |
| 15 | +```text |
| 16 | +seed_lo = sum_i R_i cos(2 pi i j / p) |
| 17 | +seed_hi = sum_i R_i sin(2 pi i j / p) |
| 18 | +X_seed = seed_lo - i seed_hi. |
| 19 | +``` |
| 20 | + |
| 21 | +For a composite child with residual size `M`, the cascade keeps `X_seed` as two real arrays rather than converting back through Chebyshev inverse coefficients. If `lo - i hi` is already the node value at parent angle `f`, then the child with index `t` and angle `phi = (f + t) / p` is |
| 22 | + |
| 23 | +```text |
| 24 | +child_lo = sum_i A_i cos(2 pi i phi) - B_i sin(2 pi i phi) |
| 25 | +child_hi = sum_i B_i cos(2 pi i phi) + A_i sin(2 pi i phi) |
| 26 | +``` |
| 27 | + |
| 28 | +where `A_i` and `B_i` are the `p` block slices of `lo` and `hi`. Equivalently, |
| 29 | + |
| 30 | +```text |
| 31 | +child_lo - i child_hi = sum_i (A_i - i B_i) exp(-2 pi i i phi). |
| 32 | +``` |
| 33 | + |
| 34 | +That is exactly the length-`p` complex DFT across the block index, evaluated with unit-modulus twiddles. The power-of-two tail then enters the normalized Bruun subtree without changing representation. |
| 35 | + |
| 36 | +## Correctness argument |
| 37 | + |
| 38 | +The implementation tracks every phase as a `fractions.Fraction` of a full turn. Therefore every recursive child phase is an exact rational expression until the single local `sin` or `cos` evaluation. Bin placement uses `round(N * f) mod N`, which is an integer for the rational phases generated by this factor tree. |
| 39 | + |
| 40 | +The cascade recurrence is algebraically exact over real arithmetic because it is just complex multiplication by `exp(-2 pi i i phi)` and addition. The real and imaginary formulas above are the expanded real form of that complex expression. The base case places `lo[0] - i hi[0]`; the power-of-two case passes the unchanged two-plane normalized seed into the existing normalized Bruun subtree. Inductively, every branch evaluates the same DFT value that the natural odd-prime factorization would evaluate. |
| 41 | + |
| 42 | +## Error sources and drift mechanisms |
| 43 | + |
| 44 | +1. **Trigonometric evaluation.** Each `_cos` and `_sin` call now evaluates the exact rational turn with mpmath at 160 decimal digits and stores the resulting binary64 value in a cache. This models a C++ table-generation path where twiddles are generated with enough precision to be correctly rounded before the FFT runs. |
| 45 | +2. **Block summation.** The prototype now uses pairwise array reductions in the odd-prime path. There is still no compensated summation or Shewchuk expansion in the transform itself, so cancellation-heavy blocks can still lose low-order bits. |
| 46 | +3. **Recursive accumulation.** Each odd stage adds `p` rounded vector terms per child. Prime powers such as `3^6` revisit the same kind of stage many times, so first-order error grows with both depth and the number of nonzero block contributions. |
| 47 | +4. **Power-of-two tail.** When `M` becomes a power of two, accuracy inherits the normalized Bruun subtree's rounding behavior. |
| 48 | +5. **Reference conversion.** The metrology script evaluates the reference DFT with mpmath at 90 decimal digits and converts the final reference bins to binary64 complex values for error measurement. RMS reductions use `math.fsum` to avoid report-side summation drift. |
| 49 | + |
| 50 | +The removed Chebyshev inverse path had a worse conditioning failure: coefficients proportional to `1 / sin(phi)` become large when a composite odd child is near a singular angle. The cascade frame replaces those coefficients with unit-modulus twiddles, so its local coefficient condition is 1. That fixes the main structural instability, but ordinary rounded summation remains. |
| 51 | + |
| 52 | +## Measured accuracy |
| 53 | + |
| 54 | +Command: |
| 55 | + |
| 56 | +```sh |
| 57 | +PYTHONPATH=. python scripts/odd_prime_accuracy_probe.py |
| 58 | +``` |
| 59 | + |
| 60 | +Reference: direct mpmath DFT at 90 decimal digits. Inputs: deterministic `standard_normal` vectors seeded by `N`. Errors: absolute bin error divided by `max(max(abs(reference)), 1)`. |
| 61 | + |
| 62 | +| N | factors | max Bruun | max NumPy | RMS Bruun | RMS NumPy | max ratio | |
| 63 | +| ---: | --- | ---: | ---: | ---: | ---: | ---: | |
| 64 | +| 3 | 3 | 6.874e-18 | 2.062e-17 | 4.861e-18 | 1.458e-17 | 0.33 | |
| 65 | +| 5 | 5 | 7.630e-17 | 3.815e-17 | 5.395e-17 | 2.203e-17 | 2.00 | |
| 66 | +| 7 | 7 | 1.103e-16 | 9.865e-17 | 7.601e-17 | 6.287e-17 | 1.12 | |
| 67 | +| 9 | 3^2 | 1.518e-16 | 1.224e-16 | 9.148e-17 | 1.044e-16 | 1.24 | |
| 68 | +| 15 | 3 * 5 | 1.621e-16 | 2.017e-16 | 1.068e-16 | 1.305e-16 | 0.80 | |
| 69 | +| 21 | 3 * 7 | 1.091e-16 | 8.987e-17 | 7.221e-17 | 5.253e-17 | 1.21 | |
| 70 | +| 25 | 5^2 | 1.463e-16 | 1.573e-16 | 8.416e-17 | 8.778e-17 | 0.93 | |
| 71 | +| 27 | 3^3 | 1.361e-16 | 1.361e-16 | 8.610e-17 | 8.492e-17 | 1.00 | |
| 72 | +| 45 | 3^2 * 5 | 1.814e-16 | 2.250e-16 | 9.514e-17 | 1.250e-16 | 0.81 | |
| 73 | +| 75 | 3 * 5^2 | 1.621e-16 | 2.563e-16 | 7.171e-17 | 1.090e-16 | 0.63 | |
| 74 | +| 81 | 3^4 | 1.244e-16 | 1.760e-16 | 7.048e-17 | 8.732e-17 | 0.71 | |
| 75 | +| 125 | 5^3 | 1.524e-16 | 2.170e-16 | 7.643e-17 | 9.807e-17 | 0.70 | |
| 76 | +| 225 | 3^2 * 5^2 | 2.260e-16 | 2.452e-16 | 9.168e-17 | 1.036e-16 | 0.92 | |
| 77 | +| 243 | 3^5 | 2.267e-16 | 3.356e-16 | 9.013e-17 | 1.208e-16 | 0.68 | |
| 78 | +| 405 | 3^4 * 5 | 2.040e-16 | 2.325e-16 | 8.717e-17 | 8.971e-17 | 0.88 | |
| 79 | +| 729 | 3^6 | 3.046e-16 | 3.137e-16 | 8.514e-17 | 1.114e-16 | 0.97 | |
| 80 | + |
| 81 | +## Bounds and comparison with a typical T-C FFT |
| 82 | + |
| 83 | +A first-order floating-point model gives a local `p`-term child sum error on the order of `gamma_{O(p)} * sum_i |block_i|`, where `gamma_m = m eps / (1 - m eps)`. Because the cascade twiddles have magnitude 1, the coefficient growth factor at the odd split is approximately 1 rather than `1 / |sin(phi)|`. Across a path with odd radices `p_1, ..., p_s` and a power-of-two tail, the expected forward error is therefore proportional to |
| 84 | + |
| 85 | +```text |
| 86 | +O(eps * (sum_r p_r + C_bruun log2(M_tail))) * moderate data-growth factors. |
| 87 | +``` |
| 88 | + |
| 89 | +That is the same qualitative stability class as a standard mixed-radix Cooley-Tukey FFT. With correctly generated twiddles, the current prototype has a comparable or smaller constant than NumPy's implementation on most measured composite odd cases, while isolated prime leaves can still be worse in max-error ratio. |
| 90 | + |
| 91 | +## Conclusion |
| 92 | + |
| 93 | +The condition-1 cascade is mathematically correct and removes the composite odd-node Chebyshev singularity. It should remain the candidate structure for a C++ port. With pairwise reductions and correctly generated twiddles, the prototype now meets or exceeds NumPy/T-C accuracy on most sampled composite odd and prime-power cases, but it is still not a formal correctly rounded FFT and still has prime-leaf cases where NumPy wins by max error. Before porting as an accuracy-superior implementation, the next work should evaluate compensated odd child reductions, FMA kernels, and a larger automated mpmath/Shewchuk-style regression suite. |
| 94 | + |
| 95 | +## Follow-up design notes: reducing accumulation |
| 96 | + |
| 97 | +The prototype now uses pairwise array summation for the odd-prime DC branch, condition-1 seed construction, and cascade child construction. This changes the reduction depth from a left-associated `p - 1` additions to approximately `ceil(log2(p))` rounded levels. It does not make the result correctly rounded, but it reduces the expected summation constant and better matches how a SIMD C++ implementation should reduce vector lanes and radix blocks. |
| 98 | + |
| 99 | +For the C++ port, block summation is likely manageable rather than a fundamental blocker if the implementation uses a fixed pairwise reduction tree: |
| 100 | + |
| 101 | +1. For small odd radices, keep the explicit radix kernel and sum in a balanced order. |
| 102 | +2. For larger prime radices, accumulate real and imaginary planes with a deterministic vector tree, then horizontally reduce lanes with pairwise or compensated lane reduction. |
| 103 | +3. Use FMA for `a * c - b * s` and `b * c + a * s` when the target permits it, because each child term then has one final rounding after the product-add expression instead of separate rounded multiply and add steps. |
| 104 | +4. Avoid recurrence-generated twiddles in the accuracy path. Generate or table exact-rational phases so each twiddle is rounded independently from the intended phase. |
| 105 | + |
| 106 | +Correct rounding of the whole FFT is a stronger target than FFT-grade stability. To claim correct rounding for every output bin, the implementation would need a reproducible error bound below one half ulp for each bin after all twiddle, product, and summation errors. In practice that means either adaptive extended precision for difficult bins, expansion arithmetic/Shewchuk-style summation in the odd reductions, correctly rounded or tightly bounded sin/cos tables, and interval/error tracking through the Bruun subtree. Pairwise SIMD reduction alone is not enough for a formal correct-rounding claim. |
| 107 | + |
| 108 | +## Pairwise-summation probe update |
| 109 | + |
| 110 | +A short probe run during the pairwise change showed the same pattern on the smaller sample: |
| 111 | + |
| 112 | +| N | factors | max Bruun | max NumPy | RMS Bruun | RMS NumPy | max ratio | |
| 113 | +| ---: | --- | ---: | ---: | ---: | ---: | ---: | |
| 114 | +| 3 | 3 | 1.555e-16 | 2.062e-17 | 1.101e-16 | 1.458e-17 | 7.54 | |
| 115 | +| 5 | 5 | 1.145e-16 | 3.815e-17 | 8.531e-17 | 2.203e-17 | 3.00 | |
| 116 | +| 9 | 3^2 | 3.643e-16 | 1.224e-16 | 1.816e-16 | 1.044e-16 | 2.98 | |
| 117 | +| 15 | 3 * 5 | 2.087e-16 | 2.017e-16 | 1.347e-16 | 1.305e-16 | 1.03 | |
| 118 | +| 27 | 3^3 | 6.805e-16 | 1.361e-16 | 3.153e-16 | 8.492e-17 | 5.00 | |
| 119 | +| 45 | 3^2 * 5 | 7.374e-16 | 2.250e-16 | 3.579e-16 | 1.250e-16 | 3.28 | |
| 120 | +| 75 | 3 * 5^2 | 5.730e-16 | 2.563e-16 | 2.518e-16 | 1.090e-16 | 2.24 | |
| 121 | +| 81 | 3^4 | 6.700e-16 | 1.760e-16 | 2.994e-16 | 8.732e-17 | 3.81 | |
| 122 | +| 125 | 5^3 | 4.514e-16 | 2.170e-16 | 2.019e-16 | 9.807e-17 | 2.08 | |
| 123 | +| 225 | 3^2 * 5^2 | 7.232e-16 | 2.452e-16 | 2.966e-16 | 1.036e-16 | 2.95 | |
| 124 | + |
| 125 | +The improvement is visible for radix-5-heavy cases, especially `N = 5`, `75`, `125`, and `225`. Radix-3 powers are mostly unchanged because a three-term balanced sum still has two addition levels and the dominant error is often twiddle/product rounding or inherited subtree error. The conclusion therefore remains conservative: pairwise summation is a necessary C++ design choice, but not sufficient by itself to beat NumPy/T-C across the odd-prime stack. |
| 126 | + |
| 127 | + |
| 128 | +## Correctly rounded twiddle update |
| 129 | + |
| 130 | +The final prototype round changes twiddle generation from `np.cos(2*pi*float(f))` and `np.sin(2*pi*float(f))` to a cached high-precision path. Each exact rational turn is evaluated with mpmath at 160 decimal digits, converted once to binary64, and then reused from the cache. This models the intended C++ assumption that plan-time or offline twiddle tables are generated correctly and stored as doubles before SIMD kernels consume them. |
| 131 | + |
| 132 | +This change extracts most of the remaining accuracy benefit. The largest measured Bruun/NumPy max-error ratio dropped from 7.54 in the libm-twiddle pairwise prototype to 2.00 with high-precision twiddles, and the composite prime-power cases in the table are now generally at or below NumPy. The remaining gap is no longer dominated by phase generation; it is mostly the final rounded product/add schedule, cancellation in small prime leaves, and the inherited power-of-two subtree. |
0 commit comments