packages feed

brush-strokes-0.1.0.0: src/lib/Math/Root/Isolation/Newton.hs

{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE UndecidableInstances #-}

module Math.Root.Isolation.Newton
  ( -- * The interval Newton method,
    -- with Gauss–Seidel step or explicit linear programming
    Newton
  , intervalNewton

    -- ** Configuration options
  , NewtonOptions(..)
  , defaultNewtonOptions
  )
  where

-- base
import Prelude hiding ( unzip )
import Control.Arrow
  ( first )
import Data.Bifunctor
  ( Bifunctor(bimap) )
import Data.Kind
  ( Type )
import Data.List
  ( partition )
import GHC.TypeNats
  ( Nat, KnownNat, type (<=) )

-- deepseq
import Control.DeepSeq
  ( force )

-- transformers
import Control.Monad.Trans.Writer.CPS
  ( Writer, tell )

-- brush-strokes
import Math.Algebra.Dual
  ( D )
import Math.Interval
import Math.Linear
import Math.Module
  ( Module(..) )
import Math.Monomial
  ( MonomialBasis(..), linearMonomial, zeroMonomial )
import Math.Root.Isolation.Core
import Math.Root.Isolation.Newton.GaussSeidel
import Math.Root.Isolation.Newton.LP
import Math.Root.Isolation.Utils

--------------------------------------------------------------------------------
-- Interval Newton

-- | The interval Newton method; see 'intervalNewton'.
data Newton
instance (Show (NewtonOptions n d), BoxCt n d) => RootIsolationAlgorithm Newton n d where
  type instance StepDescription Newton = (String, TimeInterval)
  type instance RootIsolationAlgorithmOptions Newton n d = NewtonOptions n d
  rootIsolationAlgorithm opts _thisRoundHist _prevRoundsHist eqs box =
    first ( "Newton " ++ show opts, ) <$> intervalNewton @n @d opts eqs box
  {-# INLINEABLE rootIsolationAlgorithm #-}
  {-# SPECIALISE rootIsolationAlgorithm @2 @3 #-}
    -- NB: including this to be safe. The specialiser seems to sometimes
    -- be able to generate this specialisation on its own, and sometimes not.

-- | Options for the interval Newton method.
type NewtonOptions :: Nat -> Nat -> Type
data NewtonOptions n d where
  -- | Use the Gauss–Seidel method to solve linear systems.
  NewtonGaussSeidel
    :: GaussSeidelOptions n d -> NewtonOptions n d
  -- | Use linear programming to solve linear systems (2 dimensions only).
  NewtonLP
    :: NewtonOptions 2 d

deriving stock instance Show ( NewtonOptions n d )

-- | Default options for the interval Newton method.
defaultNewtonOptions
  :: forall n d
  .  ( KnownNat n, KnownNat d
     , 1 <= n, 1 <= d, n <= d
     , Representable Double ( ℝ n )
     , Representable Double ( ℝ d )
     , Representable 𝕀 ( 𝕀ℝ n )
     , Representable 𝕀 ( 𝕀ℝ d )
     )
  => BoxHistory n
  -> NewtonOptions n d
defaultNewtonOptions history =
  NewtonGaussSeidel $ defaultGaussSeidelOptions history
{-# INLINEABLE defaultNewtonOptions #-}

-- | Interval Newton method with Gauss–Seidel step.
intervalNewton
  :: forall n d
  .  BoxCt n d
  => NewtonOptions n d
  -> ( 𝕀ℝ n -> D 1 n ( 𝕀ℝ d ) )
      -- ^ equations
  -> 𝕀ℝ n
     -- ^ box
  -> Writer ( DoneBoxes n ) ( TimeInterval, [ 𝕀ℝ n ] )
intervalNewton opts eqs x = case opts of
  NewtonGaussSeidel
    ( GaussSeidelOptions
      { gsPreconditioner = precondMeth
      , gsPickEqs = pickEqs
      , gsUpdate
      } ) ->
    let x_mid = point $ boxMidpoint x
        f :: 𝕀ℝ n -> 𝕀ℝ n
        f = \ x_0 -> pickEqs $ eqs x_0 `monIndex` zeroMonomial
        f'_x :: Vec n ( 𝕀ℝ n )
        f'_x = fmap ( \ i -> pickEqs $ eqs x `monIndex` linearMonomial i ) ( universe @n )

        -- Interval Newton method: take one Gauss–Seidel step
        -- for the system of equations f'(x) ( x - x_mid ) = - f(x_mid).
        minus_f_x_mid = unT $ -1 *^ T ( boxMidpoint $ f x_mid )

        -- Precondition the above linear system into A ( x - x_mid ) = B.
        !( !a, !b ) = force $
           precondition precondMeth
            f'_x ( point minus_f_x_mid )
        !x'_0 = force ( T x ^-^ T x_mid )

        -- NB: we have to change coordinates, putting the midpoint of the box
        -- at the origin, in order to take a Gauss–Seidel step.
        ( x's, dt ) =
          timeInterval
            ( gaussSeidelUpdate gsUpdate a b )
            x'_0
        gsGuesses = map ( first ( \ x' -> unT $ x' ^+^ T x_mid ) ) x's
        ( done, todo ) = bimap ( map fst ) ( map fst )
                       $ partition snd gsGuesses
    in -- If the Gauss–Seidel step was a contraction, then the box
       -- contains a unique solution (by the Banach fixed point theorem).
       --
       -- These boxes can thus be directly added to the solution set:
       -- Newton's method is guaranteed to converge to the unique solution.
      do tell $ noDoneBoxes { doneSolBoxes = done }
         return ( dt, todo )
  NewtonLP ->
    -- TODO: reduce duplication with the above.
    let x_mid = point $ boxMidpoint x
        f :: 𝕀ℝ 2 -> 𝕀ℝ d
        f = \ x_0 -> eqs x_0 `monIndex` zeroMonomial
        f'_x :: Vec 2 ( 𝕀ℝ d )
        f'_x = fmap ( \ i -> eqs x `monIndex` linearMonomial i ) ( universe @2 )

        minus_f_x_mid = unT $ -1 *^ T ( boxMidpoint $ f x_mid )
        !( !a, !b ) = force ( f'_x, point minus_f_x_mid )
        !x'_0 = force ( T x ^-^ T x_mid )
        ( x's, dt ) =
          timeInterval
            ( solveIntervalLinearEquations a b ) x'_0
        lpGuesses = map ( first ( \ x' -> unT $ x' ^+^ T x_mid ) ) x's
        ( done, todo ) = bimap ( map fst ) ( map fst )
                       $ partition snd lpGuesses
    in do tell $ noDoneBoxes { doneSolBoxes = done }
          return ( dt, todo )
{-# INLINEABLE intervalNewton #-}
{-# SPECIALISE intervalNewton @2 @3 #-}

{-

mbDeg = topologicalDegree 0.005 f x
det = case f'_x of
        Vec [ c1, c2 ] ->
          let a_11 = c1 `index` Fin 1
              a_12 = c2 `index` Fin 1
              a_21 = c1 `index` Fin 2
              a_22 = c2 `index` Fin 2
          in a_11 * a_22 - a_12 * a_21
        _ -> error "TODO: just testing n=2 here"

if | not $ 0 ∈ det
   , mbDeg == Just 0
   -> return []
     -- If the Jacobian is invertible over the box, then the topological
     -- degree tells us exactly how many solutions there are in the box.
-}