nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
poly_helpers.hpp
1// ops/include/nk/ops/poly_helpers.hpp
2//
3// Polynomial helpers shared between the public roots() builtin and the
4// DSP-side filter design code (tf2sos / tf2zp / butter). All-inline; no
5// matching .cpp.
6
7#pragma once
8
9#include <nk/value/scratch.hpp>
10
11// Complex comes from <complex> below; this header is value+ops-only (no core).
12
13#include <algorithm>
14#include <cmath>
15#include <complex>
16#include <cstddef>
17#include <memory_resource>
18
19namespace nk::ops {
20
21using Complex = std::complex<double>;
22
23// Horner evaluation of a polynomial p(z) and its derivative p'(z). The
24// coefficients are in MATLAB / numpy order: c[0] is the leading
25// coefficient (highest power), c[n] is the constant. Pointer + size
26// instead of a container ref so it composes with std::vector,
27// std::pmr::vector, or raw arrays.
28inline void polyEvalHorner(const Complex *c, std::size_t n,
29 Complex z, Complex &outP, Complex &outDP)
30{
31 Complex p = c[0];
32 Complex dp(0.0, 0.0);
33 for (std::size_t i = 1; i < n; ++i) {
34 dp = dp * z + p;
35 p = p * z + c[i];
36 }
37 outP = p;
38 outDP = dp;
39}
40
41// Durand-Kerner simultaneous root finder for a real-coefficient
42// polynomial. Coefficients in MATLAB order. Returns a vector of length
43// equal to the polynomial degree; complex-conjugate pairs are not
44// guaranteed to come out exactly conjugate (they're each found
45// independently), but typically agree to ~1e-13.
46//
47// Behaviour at edge cases:
48// * Leading zeros are stripped (degree drop).
49// * Trailing zeros become explicit roots at the origin.
50// * Constant or empty input → no roots.
51//
52// ~50-200 iterations to ~1e-14 tolerance for moderate polynomial
53// degrees (≤ 32). Higher degrees may need a tighter inner loop, but
54// practical SOS / roots() usage rarely exceeds this.
55inline ScratchVec<Complex>
56polyRootsDurandKerner(std::pmr::memory_resource *mr,
57 const double *coeffs, std::size_t coeffsN)
58{
59 std::size_t lo = 0, hi = coeffsN;
60 while (lo < hi && coeffs[lo] == 0.0) ++lo;
61 std::size_t origZeros = 0;
62 while (hi > lo && coeffs[hi - 1] == 0.0) { --hi; ++origZeros; }
63 if (hi - lo <= 1)
64 return ScratchVec<Complex>(origZeros, Complex(0.0, 0.0), mr);
65
66 ScratchVec<Complex> c(hi - lo, mr);
67 for (std::size_t i = 0; i < c.size(); ++i)
68 c[i] = Complex(coeffs[lo + i] / coeffs[lo], 0.0);
69 const std::size_t n = c.size() - 1;
70
71 // Initial guesses on the Cauchy-bound circle, offset to avoid
72 // a real-axis cluster (factor of e^{j·0.4} chosen empirically).
73 double maxCoef = 0.0;
74 for (std::size_t i = 1; i <= n; ++i)
75 maxCoef = std::max(maxCoef, std::abs(c[i].real()));
76 const double R = 1.0 + maxCoef;
77 ScratchVec<Complex> roots(n, mr);
78 constexpr double kTwoPi = 6.28318530717958647692;
79 for (std::size_t k = 0; k < n; ++k) {
80 const double theta = kTwoPi * static_cast<double>(k)
81 / static_cast<double>(n) + 0.4;
82 roots[k] = Complex(R * std::cos(theta), R * std::sin(theta));
83 }
84
85 constexpr int kMaxIter = 200;
86 constexpr double kTol = 1e-14;
87 for (int it = 0; it < kMaxIter; ++it) {
88 double maxStep = 0.0;
89 for (std::size_t k = 0; k < n; ++k) {
90 Complex p, dp;
91 polyEvalHorner(c.data(), c.size(), roots[k], p, dp);
92 Complex denom(1.0, 0.0);
93 for (std::size_t j = 0; j < n; ++j)
94 if (j != k) denom *= (roots[k] - roots[j]);
95 if (std::abs(denom) < 1e-300) continue;
96 const Complex step = p / denom;
97 roots[k] -= step;
98 const double s = std::abs(step);
99 if (s > maxStep) maxStep = s;
100 }
101 if (maxStep < kTol) break;
102 }
103 for (std::size_t i = 0; i < origZeros; ++i)
104 roots.push_back(Complex(0.0, 0.0));
105 return roots;
106}
107
108// Expand a list of polynomial roots into a real-coefficient polynomial
109// via repeated convolution by (x - r). Output length = rootsN + 1
110// in MATLAB order (leading coefficient first). Imaginary residue is
111// dropped — caller is responsible for ensuring the roots come in
112// complex-conjugate pairs (otherwise the imaginary parts won't cancel
113// to zero).
114inline ScratchVec<double>
115polyExpandFromRoots(std::pmr::memory_resource *mr,
116 const Complex *roots, std::size_t rootsN)
117{
118 const int n = static_cast<int>(rootsN);
119 ScratchVec<Complex> poly(static_cast<std::size_t>(n + 1), mr);
120 poly[0] = Complex(1.0, 0.0);
121 for (int i = 0; i < n; ++i)
122 for (int j = i + 1; j >= 1; --j)
123 poly[j] = poly[j] - roots[i] * poly[j - 1];
124 ScratchVec<double> r(static_cast<std::size_t>(n + 1), mr);
125 for (int i = 0; i <= n; ++i)
126 r[i] = poly[i].real();
127 return r;
128}
129
130} // namespace nk::ops
Value real(const Value &x, std::pmr::memory_resource *mr=nullptr)
Real part of complex array (y = real(x)).
Value poly(const Value &r, std::pmr::memory_resource *mr=nullptr)
Polynomial with specified roots or characteristic polynomial of a matrix.
Value roots(const Value &p, std::pmr::memory_resource *mr=nullptr)
Computes polynomial roots (roots(p)).