poly design

This page explains why evaluating a luna-poly polynomial on dual numbers yields its derivative, what the dense and sparse evaluation orders compute, how accurate the result is, and why the bridge is limited to one variable.

Design goal

Give luna-poly users the value and the derivative of a polynomial at a point with no new polynomial representation and no symbolic step, by reusing the evaluation algorithms luna-poly already has.

Mathematical background

Evaluation over dual numbers is the derivative

For p(x)=∑k=0nckxkp(x) = \sum_{k=0}^{n} c_k x^k over a commutative semiring RR, the dual design shows

p(a+bε)=p(a)+p′(a) b ε,p′(x)=∑k=1nk ckxk−1,p(a + b\varepsilon) = p(a) + p'(a)\,b\,\varepsilon, \qquad p'(x) = \sum_{k=1}^{n} k\,c_k x^{k-1},

where k ckk\,c_k means ckc_k added kk times. The identity is algebraic, so it holds exactly for integer polynomials and up to rounding for floating-point ones; it needs neither limits nor division.

Horner’s rule on dual numbers

DensePolynomial::eval computes rn+1=0r_{n+1} = 0, rk=rk+1 x+ckr_k = r_{k+1}\,x + c_k for k=n,…,0k = n, \dots, 0, and returns r0=p(x)r_0 = p(x). With x=a+bεx = a + b\varepsilon, the lifted coefficients ck+0εc_k + 0\varepsilon, and rk=pk+qkεr_k = p_k + q_k\varepsilon, the dual product and sum give

pk=pk+1 a+ck,qk=pk+1 b+a qk+1,pn+1=qn+1=0.\begin{aligned} p_k &= p_{k+1}\,a + c_k, \\ q_k &= p_{k+1}\,b + a\,q_{k+1}, \qquad p_{n+1} = q_{n+1} = 0 . \end{aligned}

For b=1b = 1 this is the classical scheme that evaluates a polynomial and its derivative together.11 D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., section 4.6.4. By induction pk=∑i≥kciai−kp_k = \sum_{i \ge k} c_i a^{i-k} and qk=b∑i>k(i−k) ciai−k−1q_k = b\sum_{i > k} (i - k)\,c_i a^{i-k-1}, so p0=p(a)p_0 = p(a) and q0=p′(a) bq_0 = p'(a)\,b.

Sparse terms by powering

SparsePolynomial::eval sums c xec\,x^e over the stored terms and computes xex^e by binary powering. On dual numbers every product applies the product rule, so xex^e becomes ae+e ae−1b εa^e + e\,a^{e-1} b\,\varepsilon (the binomial identity of the dual design), and the sum of the terms gives p(a)+p′(a) b εp(a) + p'(a)\,b\,\varepsilon again.

Design decisions

Evaluate instead of differentiating symbolically

Problem. Users need p′(x)p'(x) at points.

Options. Build the derivative polynomial with DensePolynomial::derivative and evaluate it; evaluate pp over Dual[T].

Choice. Evaluate over Dual[T]. It returns p(x)p(x) and p′(x)p'(x) from one pass, needs only Semiring (the formal derivative in luna-poly also needs NatHomomorphism to form k ckk\,c_k), does not allocate a second polynomial per derivative order, and composes with other dual computations through eval_dual. The formal derivative remains the right tool when the derivative polynomial itself is wanted; the test suite checks that the two agree.

Reuse luna-poly evaluation

The bridge lifts the coefficients with Dual::constant, builds a DensePolynomial[Dual[T]] or SparsePolynomial[Dual[T]] with the ordinary constructors, and calls eval. It does not reimplement Horner’s rule, so any change to luna-poly evaluation (and its normalization of zero coefficients, which is why T : Eq is required) applies here too.

One variable for sparse polynomials

Problem. SparsePolynomial is multivariate, but a partial derivative needs a choice of variable, and variable identity lives in the contexts of luna-poly (VariableContext, ContextPolynomial).

Choice. The sparse bridge evaluates with the single assignment [x] and is named sparse_univariate_*. A polynomial that uses another variable makes eval abort. A multivariate API would have to take a variable context and return a gradient; it is future work. Callers with a contextual polynomial can partially evaluate the other variables first, as the repository’s integration test does.

Compatibility names

The first release named the functions derivative_at, value_and_derivative_at, eval_sparse_dual, sparse_derivative_at and sparse_value_and_derivative_at. The dense_ and sparse_univariate_ names say which representation and which kind of derivative is meant; the old names stay as plain aliases so existing code keeps working.

Correctness and invariants

Exactness in exact rings

For T with exact arithmetic (Int, BigInt, exact rationals) the results are exactly p(x)p(x) and p′(x)p'(x), by the identities above.

Rounding error of the dense evaluation

Use fl(x∘y)=(x∘y)(1+δ)\mathrm{fl}(x \circ y) = (x \circ y)(1 + \delta), ∣δ∣≤u|\delta| \le u, and the product lemma ∏(1+δi)±1=1+θk\prod (1 + \delta_i)^{\pm1} = 1 + \theta_k, ∣θk∣≤γk=ku/(1−ku)|\theta_k| \le \gamma_k = ku/(1 - ku).22 N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002, Lemma 3.1 and chapter 5.

Value. By the projection homomorphism p^0\hat p_0 is the plain Horner result. Each step multiplies the running sum by (1+δ×)(1+δ+)(1 + \delta_{\times})(1 + \delta_{+}) and adds ckc_k once rounded, so ckakc_k a^k carries 2k+12k + 1 factors for k<nk < n; the leading coefficient carries 2n2n, because the first step 0⋅a+cn0 \cdot a + c_n is exact:

p^0=∑k=0nckak(1+θ(k)),∣θ(k)∣≤γ2n,∣p^0−p(a)∣≤γ2n∑k=0n∣ck∣ ∣a∣k.\hat p_0 = \sum_{k=0}^{n} c_k a^k (1 + \theta^{(k)}), \quad |\theta^{(k)}| \le \gamma_{2n}, \qquad |\hat p_0 - p(a)| \le \gamma_{2n} \sum_{k=0}^{n} |c_k|\,|a|^k .

Derivative (b=1b = 1). The tangent step computes fl(p^k+1⋅1+fl(a q^k+1))\mathrm{fl}\big(\hat p_{k+1} \cdot 1 + \mathrm{fl}(a\,\hat q_{k+1})\big), and adding the constant ckc_k adds an exact zero to the tangent. The derivative contains kk copies of ckak−1c_k a^{k-1}, one for each step j<kj < k at which ckc_k moves from the value chain into the tangent chain. Along such a path ckc_k is rounded once when it enters, twice per value step k−1,…,j+1k - 1, \dots, j + 1, once when it moves into the tangent, and twice per tangent step j−1,…,0j - 1, \dots, 0: 1+2(k−1−j)+1+2j=2k1 + 2(k - 1 - j) + 1 + 2j = 2k factors. Hence

∣q^0−p′(a)∣≤γ2n∑k=1nk ∣ck∣ ∣a∣k−1,|\hat q_0 - p'(a)| \le \gamma_{2n} \sum_{k=1}^{n} k\,|c_k|\,|a|^{k-1} ,

the same bound as for evaluating the formal derivative by Horner’s rule. The relative error is small unless the terms cancel, that is unless p′p' is ill-conditioned at aa.

Cost

Dense: n+1n + 1 dual multiply-add steps, that is about 3n3n multiplications and 3n3n additions of T, plus one array of n+1n + 1 lifted coefficients. Sparse: per term one binary powering, O(log⁡e)O(\log e) dual products, plus one dual product and one addition.

Alternatives rejected

  • Symbolic derivative then evaluation. Two passes, an extra polynomial, and a stronger bound on T; kept as luna-poly’s own DensePolynomial::derivative for users who need the polynomial.
  • A private Horner loop. Faster by a constant, but it would duplicate luna-poly semantics and drift from them.
  • Guessing a variable for multivariate sparse polynomials. Silent partial derivatives with respect to “the first variable” would be easy to misuse; the bridge aborts instead.

Boundaries

  • Univariate only: no partial derivatives or gradients of multivariate polynomials, no ContextPolynomial support.
  • First derivatives at points only; no derivative polynomials (use luna-poly), no higher derivatives.
  • No checked variants: dense and sparse evaluation do not fail except for the sparse abort described above.
  • Dense and sparse immut representations only.

Footnotes

  1. D. E. Knuth, The Art of Computer Programming, vol. 2, 3rd ed., section 4.6.4. ↩

  2. N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002, Lemma 3.1 and chapter 5. ↩