symtegration-0.6.1: src/Symtegration/Polynomial.hs
-- |
-- Module: Symtegration.Polynomial
-- Description: Polynomials for Symtegration.
-- Copyright: Copyright 2025 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
--
-- This modules defines a type class that concrete types representing polynomials
-- should be an instance of. It includes important algorithms operating on
-- polynomials. In particular, algorithms for polynomial division and
-- the extended Euclidean algorithm are included.
module Symtegration.Polynomial
( -- * Polynomials
Polynomial (..),
monic,
mapCoefficients,
mapCoefficientsM,
-- * Algorithms
divide,
pseudoDivide,
extendedEuclidean,
diophantineEuclidean,
greatestCommonDivisor,
subresultant,
differentiate,
integrate,
squarefree,
)
where
import Data.Monoid (Sum (..))
-- $setup
-- >>> import Data.Ratio ((%), denominator, numerator)
-- >>> import Symtegration.Symbolic
-- >>> import Symtegration.Symbolic.Simplify
-- >>> import Symtegration.Polynomial.Indexed
-- | Polynomials must support the operations specified in this type class.
-- All powers must be non-negative.
class (Integral e, Num c) => Polynomial p e c where
-- | Returns the degree of a given polynomial.
--
-- The following returns 9 for the highest term in \(3x^9 + 2x^4 + x\):
--
-- >>> degree (3 * power 9 + 2 * power 4 + power 1 :: IndexedPolynomial)
-- 9
degree :: p e c -> e
-- | Returns the coefficient for the term with the given power.
--
-- The following returns 4 from the \(4x^3\) term in \(x^4 + 4x^3 + 3\):
--
-- >>> coefficient (power 4 + 4 * power 3 + 3 :: IndexedPolynomial) 3
-- 4 % 1
coefficient :: p e c -> e -> c
-- | Returns the leading coefficient.
--
-- The following returns 6 from the \(6x^3\) term in \(6x^3 + 2x^2\):
--
-- >>> leadingCoefficient (6 * power 3 + 2 * power 2 :: IndexedPolynomial)
-- 6 % 1
--
-- The leading coefficient is never zero unless the polynomial itself is zero.
leadingCoefficient :: p e c -> c
-- | Returns the polynomial without the leading term.
--
-- >>> deleteLeadingTerm (2 * power 3 + power 1 + 2 :: IndexedPolynomial)
-- x + 2
deleteLeadingTerm :: p e c -> p e c
-- | Fold the terms, i.e., the powers and coefficients, using the given monoid.
-- Only terms with non-zero coefficients will be folded.
-- Folding is ordered from lower to higher terms.
--
-- For example with \(3x^5 - 2x + 7\),
--
-- >>> foldTerms (\e c -> show (e, c)) (3 * power 5 - 2 * power 1 + 7 :: IndexedPolynomial)
-- "(0,7 % 1)(1,(-2) % 1)(5,3 % 1)"
foldTerms :: (Monoid m) => (e -> c -> m) -> p e c -> m
-- | Multiplies a polynomial by a scalar.
--
-- The following divides \(6x + 2\) by 2:
--
-- >>> scale (1 % 2) (6 * power 1 + 2 :: IndexedPolynomial)
-- 3x + 1
scale :: c -> p e c -> p e c
-- | Returns a single term with the variable raised to the given power.
--
-- The following is equivalent to \(x^5\):
--
-- >>> power 5 :: IndexedPolynomial
-- x^5
power :: e -> p e c
-- | Scale the polynomial so that its leading coefficient is one.
--
-- >>> monic $ 4 * power 2 + 4 * power 1 + 4 :: IndexedPolynomial
-- x^2 + x + 1
--
-- The exception is when the polynomial is zero.
--
-- >>> monic 0 :: IndexedPolynomial
-- 0
monic :: (Polynomial p e c, Eq c, Fractional c) => p e c -> p e c
monic p
| leadingCoefficient p == 0 = p
| otherwise = scale (1 / leadingCoefficient p) p
-- | Maps the coefficients in a polynomial to form another polynomial.
--
-- For example, it can be used to convert a polynomial with 'Rational' coefficients
-- into a polynomial with 'Expression' coefficients.
--
-- >>> let p = 2 * power 1 + 1 :: IndexedPolynomial
-- >>> let q = mapCoefficients fromRational p :: IndexedSymbolicPolynomial
-- >>> simplify $ coefficient q 1
-- Number 2
--
-- Note that only non-zero coefficients are mapped.
mapCoefficients ::
(Polynomial p e c, Polynomial p e c', Num (p e c), Num (p e c')) =>
(c -> c') ->
p e c ->
p e c'
mapCoefficients f p = getSum $ foldTerms convertTerm p
where
convertTerm e c = Sum $ scale (f c) (power e)
-- | Maps the coefficients in a polynomial to form another polynomial, but in a monad.
-- Specifically, it maps each coefficient in a monadic action,
-- and collects the products of each result and power.
--
-- For example, with the 'Maybe' monad:
--
-- >>> let f q | denominator q == 1 = Just q | otherwise = Nothing
-- >>> let p = scale 2 (power 2) + scale 3 (power 1) :: IndexedPolynomial
-- >>> mapCoefficientsM f p
-- Just 2x^2 + 3x
-- >>> let q = scale (1/2) (power 2) + scale 3 (power 1) :: IndexedPolynomial
-- >>> mapCoefficientsM f q
-- Nothing
--
-- As an another example, with the 'Either' monad:
--
-- >>> let f q | denominator q == 1 = Right q | otherwise = Left "not integer"
-- >>> let p = scale 2 (power 2) + scale 3 (power 1) :: IndexedPolynomial
-- >>> mapCoefficientsM f p
-- Right 2x^2 + 3x
-- >>> let q = scale (1/2) (power 2) + scale 3 (power 1) :: IndexedPolynomial
-- >>> mapCoefficientsM f q
-- Left "not integer"
--
-- Note that only non-zero coefficients are mapped.
mapCoefficientsM ::
(Polynomial p e c, Polynomial p e c', Num (p e c), Num (p e c'), Monad m) =>
(c -> m c') ->
p e c ->
m (p e c')
mapCoefficientsM f p = sum <$> mapM f' terms
where
terms = foldTerms (\e c -> [(e, c)]) p
f' (e, c) = do
c' <- f c
return $ scale c' $ power e
-- | Polynomial division. It returns the quotient polynomial and the remainder polynomial.
--
-- For example, dividing \(p = x^3-12x^2-42\) by \(q = x^2 - 2x + 1\)
-- returns \(x-10\) as the quotient and \(-21x-32\) as the remainder,
-- since \(p = (x-10)q -21x - 32\):
--
-- >>> let p = power 3 - 12 * power 2 - 42 :: IndexedPolynomial
-- >>> let q = power 2 - 2 * power 1 + 1 :: IndexedPolynomial
-- >>> divide p q
-- (x + (-10),(-21)x + (-32))
divide ::
(Polynomial p e c, Eq (p e c), Num (p e c), Fractional c) =>
-- | Dividend polynomial being divided.
p e c ->
-- | Divisor polynomial dividing the dividend.
p e c ->
-- | Quotient and remainder.
(p e c, p e c)
divide p q = go 0 p
where
go quotient remainder
| remainder /= 0, delta >= 0 = go (quotient + t) (remainder' - qt')
| otherwise = (quotient, remainder)
where
delta = degree remainder - degree q
t = scale (leadingCoefficient remainder / leadingCoefficient q) $ power delta
-- remainder and q * t will have the same leading coefficients.
-- Subtract them without the leading terms.
-- Not necessary for purely numeric coefficients,
-- but guarantees the cancellation of the leading terms when coefficients are symbolic.
remainder' = deleteLeadingTerm remainder
qt' = deleteLeadingTerm $ q * t
-- | Polynomial pseudo-division. It returns the pseudo-quotient and pseudo-remainder polynomials.
--
-- Equivalent to \(b^{\delta+1} p\) divided by \(q\),
-- where \(p\) and \(q\) are polynomials with integer coefficients,
-- \(b\) is the leading coefficient of \(q\) and \(\delta=\max(-1, \deg(p) - \deg(q))\).
-- This guarantees the pseudo-quotient and pseudo-remainder exist,
-- even when the quotient and remainder do not when only integer coefficients are allowed.
--
-- For example, with \(p = 3x^3 + x^2 + x + 5\) and \(q = 5x^2 - 3x + 1\),
-- it is the case that \(5^2p = (15x + 14)q + (52x + 111)\):
--
-- >>> let p = 3 * power 3 + power 2 + power 1 + 5 :: IndexedPolynomial
-- >>> let q = 5 * power 2 - 3 * power 1 + 1 :: IndexedPolynomial
-- >>> pseudoDivide p q
-- (15x + 14,52x + 111)
pseudoDivide ::
(Polynomial p e c, Eq (p e c), Num (p e c), Num c) =>
-- | Dividend polynomial being pseudo-divided.
p e c ->
-- | Divisor polynomial pseudo-dividing the dividend.
p e c ->
-- | Pseudo-quotient and pseudo-remainder.
(p e c, p e c)
pseudoDivide p q
| degree p < degree q = (0, p)
| otherwise = go (1 + degree p - degree q) 0 p
where
b = leadingCoefficient q
go n quotient remainder
| remainder /= 0, delta >= 0 = go (n - 1) quotient' remainder'
| otherwise = (scale (b ^ n) quotient, scale (b ^ n) remainder)
where
delta = degree remainder - degree q
t = scale (leadingCoefficient remainder) (power delta)
quotient' = scale b quotient + t
-- Subtract with the leading terms deleted.
-- The leading terms cancel out numerically,
-- but guarantee cancellation when the coefficients are symbolic.
remainder' = deleteLeadingTerm (scale b remainder) - deleteLeadingTerm (t * q)
-- | The extended Euclidean algorithm. For polynomials \(p\) and \(q\),
-- it returns the greatest common divisor between \(p\) and \(q\).
-- It also returns \(s\) and \(t\) such that \(sp+tq = \gcd(p,q)\).
--
-- For example, for \(p=2x^5-2x\) and \(q=x^4-2x^2+1\), it is the case
-- that \(\gcd(p,q)=-x^2+1\) and \((-\frac{1}{4}x) p + (\frac{1}{2}x^2 + 1) q = -x^2+1\):
--
-- >>> let p = 2 * power 5 - 2 * power 1 :: IndexedPolynomial
-- >>> let q = power 4 - 2 * power 2 + 1 :: IndexedPolynomial
-- >>> extendedEuclidean p q
-- (((-1) % 4)x,(1 % 2)x^2 + 1,(-1)x^2 + 1)
extendedEuclidean ::
(Polynomial p e c, Eq (p e c), Num (p e c), Fractional c) =>
-- | Polynomial \(p\).
p e c ->
-- | Polynomial \(q\).
p e c ->
-- | \(s\), \(t\), and \(\gcd(p,q)\).
(p e c, p e c, p e c)
extendedEuclidean u v = descend u v 1 0 0 1
where
descend g 0 s t _ _ = (s, t, g)
descend a b a1 a2 b1 b2 = descend b r b1 b2 r1 r2
where
(q, r) = divide a b
r1 = a1 - q * b1
r2 = a2 - q * b2
-- | Solves \(sa + tb = c\) for given polynomials \(a\), \(b\), and \(c\).
-- It will be the case that either \(s=0\) or
-- the degree of \(s\) will be less than the degree of \(b\).
--
-- >>> let a = power 4 - 2 * power 3 - 6 * power 2 + 12 * power 1 + 15 :: IndexedPolynomial
-- >>> let b = power 3 + power 2 - 4 * power 1 - 4 :: IndexedPolynomial
-- >>> let c = power 2 - 1 :: IndexedPolynomial
-- >>> diophantineEuclidean a b c
-- Just (((-1) % 5)x^2 + (4 % 5)x + ((-3) % 5),(1 % 5)x^3 + ((-7) % 5)x^2 + (16 % 5)x + (-2))
--
-- If there is no such \((s,t)\), then 'Nothing' is returned.
diophantineEuclidean ::
(Polynomial p e c, Eq (p e c), Num (p e c), Fractional c) =>
-- | Polynomial \(a\).
p e c ->
-- | Polynomial \(b\).
p e c ->
-- | Polynomial \(c\).
p e c ->
-- | \((s,t)\) such that \(sa + tb = c\).
Maybe (p e c, p e c)
diophantineEuclidean a b c
| r /= 0 = Nothing
| s' /= 0, degree s' >= degree b = Just (r', t' + q' * a)
| otherwise = Just (s', t')
where
(s, t, g) = extendedEuclidean a b
(q, r) = divide c g
s' = q * s
t' = q * t
(q', r') = divide s' b
-- | Returns the greatest common divisor btween two polynomials.
--
-- Convenient wrapper over 'extendedEuclidean' which only returns the greatest common divisor.
greatestCommonDivisor ::
(Polynomial p e c, Eq (p e c), Num (p e c), Fractional c) =>
-- | Polynomial \(p\).
p e c ->
-- | Polynomial \(q\).
p e c ->
-- | \(\gcd(p,q)\).
p e c
greatestCommonDivisor p q = g
where
(_, _, g) = extendedEuclidean p q
-- | Returns the resultant and the subresultant polynomial remainder sequence for the given polynomials.
--
-- >>> subresultant (power 2 + 1) (power 2 - 1 :: IndexedPolynomial)
-- (4 % 1,[x^2 + 1,x^2 + (-1),(-2),0])
-- >>> subresultant (2 * power 2 - 3 * power 1 + 1) (5 * power 2 + power 1 - 6 :: IndexedPolynomial)
-- (0 % 1,[2x^2 + (-3)x + 1,5x^2 + x + (-6),17x + (-17),0])
-- >>> subresultant (power 3 + 2 * power 2 + 3 * power 1 + 4) (5 * power 2 + 6 * power 1 + 7 :: IndexedPolynomial)
-- (832 % 1,[x^3 + 2x^2 + 3x + 4,5x^2 + 6x + 7,16x + 72,832,0])
--
-- === __Reference__
--
-- See sections 1.4 and 1.5 in
-- [/Symbolic Integration I: Transcendental Functions/](https://doi.org/10.1007/b138171)
-- by Manuel Bronstein for the definition of resultants, subresultants,
-- polynomial remainder sequences, and subresultant polynomial remainder sequences.
subresultant ::
(Polynomial p e c, Eq (p e c), Num (p e c), Num e, Fractional c) =>
-- | First element in the remainder sequence.
p e c ->
-- | Second element in the remainder sequence.
p e c ->
-- | The resultant and the subresultant polynomial remainder sequence.
(c, [p e c])
subresultant p q
| degree p >= degree q = (resultantFromSequence rs betas, rs)
| otherwise = ((-1) ^ (degree q * degree p) * resultant, prs)
where
(rs, betas) = subresultantRemainderSequence (p, q) gamma beta
gamma = -1
beta = (-1) ^ (1 + delta)
delta = degree p - degree q
(resultant, prs) = subresultant q p
-- | Derives the subresultant polynomial remainder sequence for 'subresultant'.
-- Constructs \(\gamma_i\), \(\beta_i\), and the remainder sequence as it goes along.
-- Returns the remainder sequence and the sequence of \(\beta_i\).
subresultantRemainderSequence ::
(Polynomial p e c, Eq (p e c), Num (p e c), Num e, Fractional c) =>
-- | The previous and current remainders in the sequence.
(p e c, p e c) ->
-- | \(\gamma_i\) as defined for the subresultant PRS.
c ->
-- | \(\beta_i\) as defined for the subresultant PRS.
c ->
-- | Polynomial remainder sequence and sequence of \(\beta_i\).
([p e c], [c])
subresultantRemainderSequence (rprev, rcurr) gamma beta
| rcurr /= 0 = (rprev : rs, beta : betas)
| otherwise = ([rprev, rcurr], [beta])
where
(rs, betas) = subresultantRemainderSequence (rcurr, rnext) gamma' beta'
(_, r) = pseudoDivide rprev rcurr
rnext = scale (1 / beta) r
lc = leadingCoefficient rcurr
delta = degree rprev - degree rcurr
delta' = degree rcurr - degree rnext
gamma' = ((-lc) ^ delta) * (gamma ^^ (1 - delta))
beta' = (-lc) * (gamma' ^ delta')
-- | Constructs the resultant based on the subresultant polynomial remainder sequence
-- and the sequence of \(\beta_i\) used to construct the subresultant PRS.
resultantFromSequence ::
(Polynomial p e c, Eq (p e c), Num (p e c), Num e, Fractional c) =>
-- | Subresultant polynomial remainder sequence.
[p e c] ->
-- | Sequence of \(\beta_i\) used for deriving the subresultant PRS.
[c] ->
-- | Resultant.
c
resultantFromSequence rs betas = go rs betas 1 1
where
go (r : r' : r'' : rs') (beta : betas') c s
| [] <- rs', degree r' > 0 = 0
| [] <- rs', degree r == 1 = leadingCoefficient r'
| [] <- rs' = s * c * leadingCoefficient r' ^ degree r
| otherwise = go (r' : r'' : rs') betas' c' s'
where
s' | odd (degree r), odd (degree r') = -s | otherwise = s
c' = c * ((beta / (lc ^ (1 + delta))) ^ degree r') * (lc ^ (degree r - degree r''))
lc = leadingCoefficient r'
delta = degree r - degree r'
go _ _ _ _ = 0
-- | Returns the derivative of the given polynomial.
--
-- >>> differentiate (power 2 + power 1 :: IndexedPolynomial)
-- 2x + 1
differentiate :: (Polynomial p e c, Num (p e c), Num c) => p e c -> p e c
differentiate p = getSum $ foldTerms diffTerm p
where
diffTerm 0 _ = Sum 0
diffTerm e c = Sum $ scale (fromIntegral e * c) $ power (e - 1)
-- | Returns the integral of the given polynomial.
--
-- >>> integrate (power 2 + power 1 :: IndexedPolynomial)
-- (1 % 3)x^3 + (1 % 2)x^2
integrate :: (Polynomial p e c, Num (p e c), Fractional c) => p e c -> p e c
integrate p = getSum $ foldTerms integrateTerm p
where
integrateTerm e c = Sum $ scale (c / (1 + fromIntegral e)) $ power (e + 1)
-- | Returns the squarefree factorization of the given polynomial.
--
-- Specifically, for a polynomial \(p\), find \([p_1, p_2, \ldots, p_n]\) such that
--
-- \[ p = \sum_{k=1}^n p_k^k \]
--
-- where all \(p_k\) are squarefree, i.e., there is no polynomial \(q\) such that \(q^2 = p_k\).
--
-- For example, the squarefree factorization of \(x^8 + 6x^6 + 12x^4 + 8x^2\)
-- is \(x^2 (x^2 + 2)^3\):
--
-- >>> squarefree (power 8 + 6 * power 6 + 12 * power 4 + 8 * power 2 :: IndexedPolynomial)
-- [1,x,x^2 + 2]
squarefree :: (Polynomial p e c, Eq (p e c), Num (p e c), Eq c, Fractional c) => p e c -> [p e c]
squarefree 0 = [0]
squarefree p
| (x : xs) <- factor u v = scale c x : xs
| otherwise = [scale c 1]
where
c = leadingCoefficient p
q = scale (1 / c) p
q' = differentiate q
g = monic $ greatestCommonDivisor q q'
(u, _) = q `divide` g
(v, _) = q' `divide` g
factor s y
| z == 0 = [s]
| otherwise = f : factor s' y'
where
z = y - differentiate s
f = monic $ greatestCommonDivisor s z
(s', _) = s `divide` f
(y', _) = z `divide` f