symtegration-0.6.1: src/Symtegration/Polynomial/Solve.hs
-- |
-- Module: Symtegration.Polynomial.Solve
-- Description: Derive the roots of polynomials with rational coefficients.
-- Copyright: Copyright 2025 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
--
-- This module supports deriving exact solutions to polynomial equations.
-- It cannot derive solutions for all polynomials; it will only return those which it can.
module Symtegration.Polynomial.Solve (solve, complexSolve) where
import Data.List (nub)
import Symtegration.Polynomial
import Symtegration.Polynomial.Indexed
import Symtegration.Symbolic
-- $setup
-- >>> import Symtegration
-- >>> import Symtegration.Polynomial
-- | Derive the roots for the given polynomial. Only real roots are returned.
--
-- >>> map (toHaskell . simplify) <$> solve (2 * power 1 - 6)
-- Just ["3"]
--
-- >>> map (toHaskell . simplify) <$> solve (power 2 - 4)
-- Just ["2","-2"]
--
-- Returns 'Nothing' if the function does not know how to derive the roots.
solve :: IndexedPolynomial -> Maybe [Expression]
solve p
| degree p == 1 = solveLinear (c 1) (c 0)
| degree p == 2 = solveQuadratic (c 2) (c 1) (c 0)
| degree p == 3 = solveCubic (c 3) (c 2) (c 1) (c 0)
| degree p == 4 = solveQuartic (c 4) (c 3) (c 2) (c 1) (c 0)
| otherwise = Nothing
where
c = coefficient p
-- | Returns the real root for a polynomial of degree 1.
solveLinear :: Rational -> Rational -> Maybe [Expression]
solveLinear a b = Just [fromRational ((-b) / a)]
-- | Returns the real roots for a polynomial of degree 2.
solveQuadratic :: Rational -> Rational -> Rational -> Maybe [Expression]
solveQuadratic a b c
| sq == 0 = Just [fromRational $ (-b) / (2 * a)]
| sq > 0 =
Just
[ ((-b') + sq' ** (1 / 2)) / (2 * a'),
((-b') - sq' ** (1 / 2)) / (2 * a')
]
| otherwise = Just []
where
sq = b * b - 4 * a * c
sq' = fromRational sq
a' = fromRational a
b' = fromRational b
-- | Returns the real roots for a polynomial of degree 3.
solveCubic :: Rational -> Rational -> Rational -> Rational -> Maybe [Expression]
solveCubic a b c d = map restore <$> depressedRoots
where
restore x = x - fromRational b / (3 * fromRational a)
depressedRoots = solveDepressedCubic p q
p = (3 * a * c - b ^ two) / (3 * a ^ two)
q = (2 * b ^ three - 9 * a * b * c + 27 * a ^ two * d) / (27 * a ^ three)
two = 2 :: Int
three = 3 :: Int
-- | Solve depressed cubic equations of the form \(x^3 + px + q = 0\).
-- Only returns real roots.
--
-- #### References
--
-- * [Wikipedia](https://en.wikipedia.org/wiki/Cubic_equation#Trigonometric_and_hyperbolic_solutions)
-- * [Wolfram MathWorld](https://mathworld.wolfram.com/CubicFormula.html)
solveDepressedCubic :: Rational -> Rational -> Maybe [Expression]
solveDepressedCubic 0 q
| q < 0 = Just [fromRational (-q) ** (1 / 3)]
| otherwise = Just [negate $ fromRational q ** (1 / 3)]
solveDepressedCubic p q
| s < 0 =
let c = 2 * sqrt (-(p' / 3))
theta = acos (3 / 2 * q' / p' * sqrt (-(3 / p'))) / 3
in Just [c * cos theta, c * cos (theta - 2 * pi / 3), c * cos (theta - 4 * pi / 3)]
| p < 0,
s > 0 =
Just [(-2) * signum q' * sqrt (-(p' / 3)) * cosh (acosh ((-3) / 2 * abs q' / p' * sqrt (-(3 / p'))) / 3)]
| s == 0 = Just [fromRational (3 * q / p), fromRational ((-3) / 2 * q / p)]
| p > 0 =
Just [(-2) * sqrt (p' / 3) * sinh (asinh (3 / 2 * q' / p' * sqrt (3 / p')) / 3)]
| otherwise = Nothing
where
s = 4 * p ^ (3 :: Int) + 27 * q ^ (2 :: Int)
p' = fromRational p
q' = fromRational q
-- | Returns the real roots for a polynomial of degree 4.
solveQuartic :: Rational -> Rational -> Rational -> Rational -> Rational -> Maybe [Expression]
solveQuartic a b 0 0 0
| b /= 0 = Just [0, fromRational $ -(b / a)]
| otherwise = Just [0]
solveQuartic a b c 0 0
| (Just xs) <- solveQuadratic a b c = Just $ nub $ 0 : xs
| otherwise = Nothing
solveQuartic a b c d 0
| (Just xs) <- solveCubic a b c d = Just $ nub $ 0 : xs
| otherwise = Nothing
solveQuartic a 0 0 0 b
| a > 0, b > 0 = Just []
| a < 0, b < 0 = Just []
| b == 0 = Just [0]
| otherwise = Just [x, -x]
where
x = fromRational ((-b) / a) ** (1 / 4)
solveQuartic a 0 b 0 c
| sq < 0 = Just []
| sq == 0, st < 0 = Just []
| sq == 0 = Just [sqrt st', -sqrt st']
| a > 0, sq > 0, b > 0, sq > b * b = Just [sqrt x1, -sqrt x1]
| a < 0, sq > 0, b < 0, sq > b * b = Just [sqrt x2, -sqrt x2]
| a > 0, sq > 0, b < 0, sq < b * b = Just [sqrt x1, -sqrt x1, sqrt x2, -sqrt x2]
| a < 0, sq > 0, b > 0, sq < b * b = Just [sqrt x1, -sqrt x1, sqrt x2, -sqrt x2]
| otherwise = Nothing
where
sq = b * b - 4 * a * c
st = (-b) / (2 * a)
sq' = fromRational sq
st' = fromRational st
a' = fromRational a
b' = fromRational b
x1 = ((-b') + sqrt sq') / (2 * a')
x2 = ((-b') - sqrt sq') / (2 * a')
solveQuartic _ _ _ _ _ = Nothing
-- | Derive the roots for the given polynomial.
-- All roots are returned, including complex roots.
--
-- >>> map (toHaskell . simplify) <$> complexSolve (2 * power 1 - 6)
-- Just ["3"]
--
-- >>> map (toHaskell . simplify) <$> complexSolve (power 2 + 1)
-- Just ["(-1) ** (1 / 2)","(-1) * (-1) ** (1 / 2)"]
--
-- >>> map (toHaskell . simplify) <$> complexSolve (power 3 + 1)
-- Just ["-1","(-1) * ((-1) + (-1) ** (1 / 2) * 3 ** (1 / 2)) / 2","(-1) * ((-1) + (-1) * (-1) ** (1 / 2) * 3 ** (1 / 2)) / 2"]
--
-- Returns 'Nothing' if the function does not know how to derive the roots.
complexSolve :: IndexedPolynomial -> Maybe [Expression]
complexSolve p
| degree p == 1 = complexSolveLinear (c 1) (c 0)
| degree p == 2 = complexSolveQuadratic (c 2) (c 1) (c 0)
| degree p == 3 = complexSolveCubic (c 3) (c 2) (c 1) (c 0)
| degree p == 4 = complexSolveQuartic (c 4) (c 3) (c 2) (c 1) (c 0)
| otherwise = Nothing
where
c = coefficient p
-- | Returns the roots for a polynomial of degree 1.
complexSolveLinear :: Rational -> Rational -> Maybe [Expression]
complexSolveLinear a b = Just [fromRational $ (-b) / a]
-- | Returns the roots for a polynomial of degree 2.
complexSolveQuadratic :: Rational -> Rational -> Rational -> Maybe [Expression]
complexSolveQuadratic a b c
| sq == 0 = Just [p]
| otherwise = Just [p + q, p - q]
where
sq = b * b - 4 * a * c
p = fromRational $ (-b) / (2 * a)
q = sqrt (fromRational sq) / fromRational (2 * a)
-- | Returns the roots for a polynomial of degree 3.
complexSolveCubic :: Rational -> Rational -> Rational -> Rational -> Maybe [Expression]
complexSolveCubic _ 0 0 0 = Just [0]
complexSolveCubic a b 0 0 = Just [0, fromRational $ (-b) / a]
complexSolveCubic a b c 0
| Just xs <- complexSolveQuadratic a b c = Just $ nub $ 0 : xs
| otherwise = Just [0]
complexSolveCubic a b c d = map restore <$> complexSolveDepressedCubic p q
where
restore t = t - fromRational (b / (3 * a))
p = (3 * a * c - b * b) / (3 * a * a)
q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a)
-- | Solve depressed cubic equations of the form \(x^3 + px + q = 0\).
--
-- #### References
--
-- * [Wikipedia](https://en.wikipedia.org/wiki/Cubic_equation)
complexSolveDepressedCubic :: Rational -> Rational -> Maybe [Expression]
complexSolveDepressedCubic p q
| discriminant == 0, p == 0 = Just [0]
| discriminant == 0 = Just $ map fromRational $ nub [3 * q / p, (-3) / 2 * q / p]
| p == 0 = Just [x * e | let x = fromRational (-q) ** (1 / 3), e <- [1, e1, e2]]
| otherwise =
Just
[ c - fromRational p / (3 * c),
c * e1 - fromRational p / (3 * c * e1),
c * e2 - fromRational p / (3 * c * e2)
]
where
discriminant = -(4 * p * p * p + 27 * q * q)
c = (fromRational (-(q / 2)) + sqrt (fromRational (q * q / 4 + p * p * p / 27))) ** (1 / 3)
e1 = (-1 + sqrt (-3)) / 2
e2 = (-1 - sqrt (-3)) / 2
-- | Returns the roots for a polynomial of degree 4.
complexSolveQuartic :: Rational -> Rational -> Rational -> Rational -> Rational -> Maybe [Expression]
complexSolveQuartic _ 0 0 0 0 = Just [0]
complexSolveQuartic a b 0 0 0 = Just $ nub [0, fromRational $ -(b / a)]
complexSolveQuartic a b c 0 0
| Just xs <- complexSolveQuadratic a b c = Just $ nub $ 0 : xs
| otherwise = Just [0]
complexSolveQuartic a b c d 0
| Just xs <- complexSolveCubic a b c d = Just $ nub $ 0 : xs
| otherwise = Just [0]
complexSolveQuartic a 0 b 0 c = concatMap restore <$> complexSolveQuadratic a b c
where
restore 0 = [0]
restore x = [sqrt x, -sqrt x]
complexSolveQuartic a b c d e = map restore <$> complexSolveDepressedQuartic p q r
where
restore x = x - fromRational (b / (4 * a))
p = (-3) * b ^ two / (8 * a ^ two) + c / a
q = b ^ three / (8 * a ^ three) - b * c / (2 * a ^ two) + d / a
r = (-3) * b ^ four / (256 * a ^ four) + c * b ^ two / (16 * a ^ three) - b * d / (4 * a ^ two) + e / a
two = 2 :: Int
three = 3 :: Int
four = 4 :: Int
-- | Returns the roots for a depressed quartic equation \(x^4+ax^2+bx+c=0\).
-- Complex numbers roots are included.
--
-- #### References
--
-- * [Wikipedia](https://en.wikipedia.org/wiki/Quartic_equation#The_general_case)
complexSolveDepressedQuartic :: Rational -> Rational -> Rational -> Maybe [Expression]
complexSolveDepressedQuartic a 0 c = concatMap restore <$> complexSolveQuadratic 1 a c
where
restore 0 = [0]
restore x = [sqrt x, -sqrt x]
complexSolveDepressedQuartic a b c = do
-- Get any cubic root of the following cubic equation.
ys <- complexSolveCubic 2 (-a) (-(2 * c)) (a * c - b * b / 4)
y <- case ys of x : _ -> Just x; [] -> Nothing
-- Because b /= 0, it is the case that s /= 0.
let s = sqrt $ 2 * y - fromRational a
let t = (-2) * y - fromRational a
return
[ (1 / 2) * (-s + sqrt (t + 2 * fromRational b / s)),
(1 / 2) * (-s - sqrt (t + 2 * fromRational b / s)),
(1 / 2) * (s + sqrt (t - 2 * fromRational b / s)),
(1 / 2) * (s - sqrt (t - 2 * fromRational b / s))
]