packages feed

diagrams-solve-0.2: src/Diagrams/Solve/Polynomial.hs

-- |
-- Module      :  Diagrams.Solve.Polynomial
-- Copyright   :  (c) 2011-2015 diagrams-solve team (see LICENSE)
-- License     :  BSD-style (see LICENSE)
-- Maintainer  :  diagrams-discuss@googlegroups.com
--
-- Exact solving of low-degree (n <= 4) polynomials.
module Diagrams.Solve.Polynomial (
  quadForm,
  cubForm,
  quartForm,
  cubForm',
  quartForm',
) where

import Data.Maybe (fromMaybe, listToMaybe)
import Prelude hiding ((^))
import qualified Prelude as P ((^))

-- | The fundamental circle constant, /i.e./ ratio between a circle's
--   circumference and radius.
tau :: Floating a => a
tau = 2 * pi

-- | A specialization of (^) to Integer
--   c.f. http://comments.gmane.org/gmane.comp.lang.haskell.libraries/21164
--   for discussion. "The choice in (^) and (^^) to overload on the
--   power's Integral type... was a genuinely bad idea." - Edward Kmett
--
--   Note there are rewrite rules in GHC.Real to expand small exponents.
(^) :: Num a => a -> Integer -> a
(^) = (P.^)

-- | Utility function used to avoid singularities
aboutZero' :: (Ord a, Num a) => a -> a -> Bool
aboutZero' toler x = abs x < toler
{-# INLINE aboutZero' #-}

------------------------------------------------------------
-- Quadratic formula
------------------------------------------------------------

-- | The quadratic formula.
quadForm :: (Floating d, Ord d) => d -> d -> d -> [d]
quadForm a b c
  -- There are infinitely many solutions in this case,
  -- so arbitrarily return 0
  | a == 0 && b == 0 && c == 0 = [0]
  -- c /= 0
  | a == 0 && b == 0 = []
  -- linear
  | a == 0 = [-c / b]
  -- no real solutions
  | d < 0 = []
  -- ax^2 + c = 0
  | b == 0 = [sqrt (-c / a), -sqrt (-c / a)]
  -- multiplicity 2 solution
  | d == 0 = [-b / (2 * a)]
  -- see http://www.mpi-hd.mpg.de/astrophysik/HEA/internal/Numerical_Recipes/f5-6.pdf
  | otherwise = [q / a, c / q]
 where
  d = b ^ 2 - 4 * a * c
  q = -1 / 2 * (b + signum b * sqrt d)
{-# INLINE quadForm #-}

_quadForm_prop :: Double -> Double -> Double -> Bool
_quadForm_prop a b c = all (aboutZero' 1e-10 . eval) (quadForm a b c)
 where
  eval x = a * x ^ 2 + b * x + c

------------------------------------------------------------
-- Cubic formula
------------------------------------------------------------

-- See http://en.wikipedia.org/wiki/Cubic_formula#General_formula_of_roots

-- | Solve the cubic equation ax^3 + bx^2 + cx + d = 0, returning a
--   list of all real roots. First argument is tolerance.
cubForm' :: (Floating d, Ord d) => d -> d -> d -> d -> d -> [d]
cubForm' toler a b c d
  | aboutZero' toler a = quadForm b c d
  -- three real roots, use trig method to avoid complex numbers
  | delta > 0 = map trig [0, 1, 2]
  -- one real root of multiplicity 3
  | delta == 0 && disc == 0 = [-b / (3 * a)]
  -- two real roots, one of multiplicity 2
  | delta == 0 && disc /= 0 =
      [ (b * c - 9 * a * d) / (2 * disc)
      , (9 * a ^ 2 * d - 4 * a * b * c + b ^ 3) / (a * disc)
      ]
  -- one real root (and two complex)
  | otherwise = [-b / (3 * a) - cc / (3 * a) + disc / (3 * a * cc)]
 where
  delta = 18 * a * b * c * d - 4 * b ^ 3 * d + b ^ 2 * c ^ 2 - 4 * a * c ^ 3 - 27 * a ^ 2 * d ^ 2
  disc = 3 * a * c - b ^ 2
  qq = sqrt (-27 * (a ^ 2) * delta)
  qq' = if abs (xx + qq) > abs (xx - qq) then qq else -qq
  cc = cubert (1 / 2 * (qq' + xx))
  xx = 2 * b ^ 3 - 9 * a * b * c + 27 * a ^ 2 * d
  p = disc / (3 * a ^ 2)
  q = xx / (27 * a ^ 3)
  phi = 1 / 3 * acos (3 * q / (2 * p) * sqrt (-3 / p))
  trig k = 2 * sqrt (-p / 3) * cos (phi - k * tau / 3) - b / (3 * a)
  cubert x
    | x < 0 = -((-x) ** (1 / 3))
    | otherwise = x ** (1 / 3)
{-# INLINE cubForm' #-}

-- | Solve the cubic equation ax^3 + bx^2 + cx + d = 0, returning a
--   list of all real roots within 1e-10 tolerance
--   (although currently it's closer to 1e-5)
cubForm :: (Floating d, Ord d) => d -> d -> d -> d -> [d]
cubForm = cubForm' 1e-10
{-# INLINE cubForm #-}

_cubForm_prop :: Double -> Double -> Double -> Double -> Bool
_cubForm_prop a b c d = all (aboutZero' 1e-5 . eval) (cubForm a b c d)
 where
  eval x = a * x ^ 3 + b * x ^ 2 + c * x + d

-- Basically, however large you set the tolerance it seems
-- that quickcheck can always come up with examples where
-- the returned solutions evaluate to something near zero
-- but larger than the tolerance (but it takes it more
-- tries the larger you set the tolerance). Wonder if this
-- is an inherent limitation or (more likely) a problem
-- with numerical stability.  If this turns out to be an
-- issue in practice we could, say, use the solutions
-- generated here as very good guesses to a numerical
-- solver which can give us a more precise answer?

------------------------------------------------------------
-- Quartic formula
------------------------------------------------------------

-- Based on http://tog.acm.org/resources/GraphicsGems/gems/Roots3b/and4.c
-- as of 5/12/14, with help from http://en.wikipedia.org/wiki/Quartic_function

-- | Solve the quartic equation c4 x^4 + c3 x^3 + c2 x^2 + c1 x + c0 = 0, returning a
--   list of all real roots. First argument is tolerance.
quartForm' :: (Floating d, Ord d) => d -> d -> d -> d -> d -> d -> [d]
quartForm' toler c4 c3 c2 c1 c0
  -- obvious cubic
  | aboutZero' toler c4 = cubForm c3 c2 c1 c0
  -- x(ax^3+bx^2+cx+d)
  | aboutZero' toler c0 = 0 : cubForm c4 c3 c2 c1
  -- substitute solutions of y back to x
  | otherwise = map (\x -> x - (a / 4)) roots
 where
  -- eliminate c4: x^4+ax^3+bx^2+cx+d
  a = c3 / c4
  b = c2 / c4
  c = c1 / c4
  d = c0 / c4
  -- eliminate cubic term via x = y - a/4
  -- reduced quartic: y^4 + py^2 + qy + r = 0
  p = b - 3 / 8 * a ^ 2
  q = 1 / 8 * a ^ 3 - a * b / 2 + c
  r = (-3 / 256) * a ^ 4 + a ^ 2 * b / 16 - a * c / 4 + d

  -- \| roots of the reduced quartic
  roots
    | aboutZero' toler r =
        0 : cubForm 1 0 p q -- no constant term: y(y^3 + py + q) = 0
    | u < -toler || v < -toler = [] -- no real solutions due to square root
    | otherwise = s1 ++ s2 -- solutions of the quadratics

  -- solve the resolvent cubic - only one solution is needed
  z = fromMaybe 0 . listToMaybe $ cubForm 1 (-p / 2) (-r) (p * r / 2 - q ^ 2 / 8)

  -- solve the two quadratic equations
  -- y^2 ± v*y-(±u-z)
  u = z ^ 2 - r
  v = 2 * z - p
  u' = if aboutZero' toler u then 0 else sqrt u
  v' = if aboutZero' toler v then 0 else sqrt v
  s1 = quadForm 1 (if q < 0 then -v' else v') (z - u')
  s2 = quadForm 1 (if q < 0 then v' else -v') (z + u')
{-# INLINE quartForm' #-}

-- | Solve the quartic equation c4 x^4 + c3 x^3 + c2 x^2 + c1 x + c0 = 0, returning a
--   list of all real roots within 1e-10 tolerance
--   (although currently it's closer to 1e-5)
quartForm :: (Floating d, Ord d) => d -> d -> d -> d -> d -> [d]
quartForm = quartForm' 1e-10
{-# INLINE quartForm #-}

_quartForm_prop :: Double -> Double -> Double -> Double -> Double -> Bool
_quartForm_prop a b c d e = all (aboutZero' 1e-5 . eval) (quartForm a b c d e)
 where
  eval x = a * x ^ 4 + b * x ^ 3 + c * x ^ 2 + d * x + e

-- Same note about tolerance as for cubic