packages feed

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

{-# LANGUAGE ScopedTypeVariables #-}

module Math.Root.Isolation
  (
    -- * Main entry point: execute root isolation strategies
    isolateRootsIn
  , Box
  , DoneBoxes(..)

    -- * General API for root isolation algorithms
  , BoxCt, BoxHistory
  , RootIsolationAlgorithm(..)
  , RootIsolationAlgorithmWithOptions(..)

    -- ** Inspecting history
  , RootIsolationStep(..)

    -- ** Visualising history
  , RootIsolationTree(..)
  , showRootIsolationTree

    -- * Configuration of the root isolation methods
  , RootIsolationOptions(..), defaultRootIsolationOptions
  , defaultRootIsolationAlgorithms

    -- * The bisection method
  , Bisection
  , BisectionOptions(..), BisectionCoordPicker
  , defaultBisectionOptions

    -- * The interval Newton method
  , NewtonOptions(..)
  , defaultNewtonOptions
    -- ** Options for the Gauss–Seidel step
  , GaussSeidelOptions(..), Preconditioner(..)
  , GaussSeidelUpdateMethod(..)

    -- * Box-consistency methods

    -- ** The @box(1)@-consistency algorithm
  , Box1
  , Box1Options(..)
  , defaultBox1Options

    -- *** Narrowing operators for the @box(1)@-consistency algorithm
  , NarrowingMethod(..)
  , narrowingMethods
  , AdaptiveShavingOptions(..)
  , defaultAdaptiveShavingOptions

    -- ** The @box(2)@-consistency algorithm
  , Box2
  , Box2Options(..)
  , defaultBox2Options
  )
  where

-- base
import Data.Kind
  ( Type )
import Data.List.NonEmpty
  ( NonEmpty )
import qualified Data.List.NonEmpty as NE
  ( NonEmpty(..), last, singleton )
import GHC.TypeNats
  ( Nat )

-- parallel
--import Control.Parallel.Strategies
--  ( Strategy, using, parTraversable, rpar )

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

-- brush-strokes
import Math.Algebra.Dual
  ( D )
import Math.Interval
import Math.Linear
import Math.Module
  ( Module(..) )
import Math.Monomial
  ( MonomialBasis(..), zeroMonomial )

import Math.Root.Isolation.Bisection
import Math.Root.Isolation.Core
import Math.Root.Isolation.Narrowing
import Math.Root.Isolation.Newton
import Math.Root.Isolation.Newton.GaussSeidel

--------------------------------------------------------------------------------

-- | Options for the root isolation methods in 'isolateRootsIn'.
type RootIsolationOptions :: Nat -> Nat -> Type
newtype RootIsolationOptions n d
  = RootIsolationOptions
  { -- | Given a box and its history, return a round of root isolation strategies
    -- to run, in sequence, on this box.
    --
    -- A return value of @Left whyStop@ indicates stopping any further iterations
    -- on this box, e.g. because it is too small.
   rootIsolationAlgorithms
     :: BoxHistory n
     -> Box n
     -> Either String ( NE.NonEmpty ( RootIsolationAlgorithmWithOptions n d ) )
  }

defaultRootIsolationOptions :: forall n d. BoxCt n d => RootIsolationOptions n d
defaultRootIsolationOptions =
  RootIsolationOptions
    { rootIsolationAlgorithms = \ hist ->
        defaultRootIsolationAlgorithms minWidth ε_eq
          ( defaultNewtonOptions @n @d hist ) hist
    }
  where
    minWidth = 1e-7
    ε_eq = 5e-3
{-# INLINEABLE defaultRootIsolationOptions #-}

defaultRootIsolationAlgorithms
  :: forall n d
  .  BoxCt n d
  => Double -- ^ minimum width of boxes (don't bisect further)
  -> Double -- ^ threshold for progress
  -> NewtonOptions n d -- ^ options for Newton's method
  -> BoxHistory n
  -> Box n
  -> Either String ( NE.NonEmpty ( RootIsolationAlgorithmWithOptions n d ) )
defaultRootIsolationAlgorithms minWidth ε_eq newtonOptions history box =
  case history of
    lastRoundBoxes : _
      -- If, in the last round of strategies, we didn't try bisection...
      | all ( \case { IsolationStep @Bisection _ -> False; _ -> True } . fst ) lastRoundBoxes
      , ( _step, lastRoundFirstBox ) <- NE.last lastRoundBoxes
      -- ...and the last round didn't sufficiently reduce the size of the box...
      , not $ box `sufficientlySmallerThan` lastRoundFirstBox
      -- ...then try bisecting the box...
      -- ...unless the box is already too small, in which case we give up.
      -> if verySmall
         then Left $ "widths <= " ++ show minWidth
         else Right $ NE.singleton $ AlgoWithOptions @Bisection _bisOptions
    -- Otherwise, do a normal round.
    -- Currently: we try an interval Gauss–Seidel.
    -- (box(1)- and box(2)-consistency don't seem to help when using
    -- the complete interval union Gauss–Seidel step)
    _ -> Right $ AlgoWithOptions @Newton newtonOptions
            NE.:| []

  where
    verySmall = and $ ( \ cd -> width cd <= minWidth ) <$> coordinates box

    _bisOptions    = defaultBisectionOptions @n @d minWidth ε_eq box
    _box1Options   = defaultBox1Options @n @d ( minWidth * 100 ) ε_eq
    _box2Options   = ( defaultBox2Options @n @d minWidth ε_eq ) { box2LambdaMin = 0.001 }

    -- Did we reduce the box width by at least ε_eq
    -- in at least one of the coordinates?
    sufficientlySmallerThan :: Box n -> Box n -> Bool
    b1 `sufficientlySmallerThan` b2 =
      or $
        ( \ cd1 cd2 -> width cd2 - width cd1 >= ε_eq )
          <$> coordinates b1
          <*> coordinates b2
{-# INLINEABLE defaultRootIsolationAlgorithms #-}

--------------------------------------------------------------------------------
-- Main driver

-- | Use various methods using interval arithmetic in order to solve a system of
-- non-linear equations.
--
-- Currently supported algorithms:
--
--  - interval Newton method with Gauss–Seidel step for inversion
--    of the interval Jacobian,
--  - coordinate-wise Newton method (@box(1)@-consistency algorithm),
--  - @box(2)@-consistency algorithm,
--  - bisection.
--
-- Returns @(tree, DoneBoxes sols dunno)@ where @tree@ is the full tree
-- (useful for debugging), sols are boxes that contain a unique solution
-- (and to which Newton's method will converge starting from anywhere inside
-- the box), and @dunno@ are boxes that we have given up processing (usually
-- because they are too small); they may or may not enclose solutions.
isolateRootsIn
  :: forall n d
  .  BoxCt n d
  => RootIsolationOptions n d
      -- ^ configuration (which algorithms to use, and with what parameters)
  -> ( 𝕀ℝ n -> D 1 n ( 𝕀ℝ d ) )
      -- ^ equations to solve
  -> Box n
      -- ^ initial search domain
  -> ( [ ( Box n, RootIsolationTree ( Box n ) ) ], DoneBoxes n )
isolateRootsIn ( RootIsolationOptions { rootIsolationAlgorithms } )
  eqs initBox = runWriter $ map ( initBox , ) <$> go [] initBox

  where

    go :: BoxHistory n
       -> Box n -- box to work on
       -> Writer ( DoneBoxes n )
                 [ RootIsolationTree ( Box n ) ]
    go history cand
      | -- Check the range of the equations contains zero.
        not $ unT ( origin :: T ( ℝ d ) ) ∈∈ iRange
        -- Box doesn't contain a solution: discard it.
      = return [ RootIsolationLeaf "rangeTest" cand ]
      | otherwise
      = case rootIsolationAlgorithms history cand of
          Right strats -> doStrategies history strats cand
          Left whyStop -> do
            -- We are giving up on this box (e.g. because it is too small).
            tell $ noDoneBoxes { doneGiveUpBoxes = [ ( cand, whyStop ) ] }
            return [ RootIsolationLeaf whyStop cand ]
      where
        iRange :: Box d
        iRange = eqs cand `monIndex` zeroMonomial

    -- Run a round of root isolation strategies, then recur.
    doStrategies
      :: BoxHistory n
      -> NonEmpty ( RootIsolationAlgorithmWithOptions n d )
      -> Box n
      -> Writer ( DoneBoxes n )
                [ RootIsolationTree ( Box n ) ]
    doStrategies prevRoundsHist = do_strats []
      where
        do_strats :: [ ( RootIsolationStep, Box n ) ]
                  -> NE.NonEmpty ( RootIsolationAlgorithmWithOptions n d )
                  -> Box n
                  -> Writer ( DoneBoxes n )
                            [ RootIsolationTree ( Box n )]
        do_strats thisRoundHist ( algo NE.:| algos ) box = do
          -- Run one strategy in the round.
          ( step, boxes ) <- doStrategy algo thisRoundHist prevRoundsHist eqs box
          case algos of
            -- If there are other algorithms to run in this round, run them next.
            nextAlgo : otherAlgos ->
              let thisRoundHist' = ( step, box ) : thisRoundHist
              in recur step ( do_strats thisRoundHist' ( nextAlgo NE.:| otherAlgos ) ) boxes
            -- Otherwise, we have done one full round of strategies.
            -- Recur back to the top (calling 'go').
            [] ->
              let thisRoundHist' = ( step, box ) NE.:| thisRoundHist
                  history = thisRoundHist' : prevRoundsHist
              in recur step ( go history ) boxes

    recur :: RootIsolationStep
          -> ( Box n -> Writer ( DoneBoxes n ) [ RootIsolationTree ( Box n ) ] )
          -> [ Box n ]
          -> Writer ( DoneBoxes n ) [ RootIsolationTree ( Box n ) ]
    recur step r cands = do
      let rest :: [ ( [ ( Box n, RootIsolationTree ( Box n ) ) ], DoneBoxes n ) ]
          rest =
            map ( \ c -> runWriter $ do { trees <- r c; return [ (c, tree) | tree <- trees ] } ) cands
          ( rest2, dones ) = unzip rest --( rest `using` myParTraversable rpar )
      tell ( mconcat dones )
      return [ RootIsolationStep step $ concat rest2 ]
{-# INLINEABLE isolateRootsIn #-}

--myParTraversable :: Strategy a -> Strategy [a]
--myParTraversable _ [] = return []
--myParTraversable strat (a:as) = (a:) <$> parTraversable strat as

-- | Execute a root isolation strategy, replacing the input box with
-- (hopefully smaller) output boxes.
doStrategy
  :: forall n d
  .  RootIsolationAlgorithmWithOptions n d
  -> [ ( RootIsolationStep, Box n ) ]
  -> BoxHistory n
  -> ( 𝕀ℝ n -> D 1 n ( 𝕀ℝ d ) )
  -> Box n
  -> Writer ( DoneBoxes n )
            ( RootIsolationStep, [ Box n ] )
doStrategy algo thisRoundHist prevRoundsHist eqs cand =
  case algo of
    AlgoWithOptions @algo options -> do
      ( step, res ) <- rootIsolationAlgorithm @algo options thisRoundHist prevRoundsHist eqs cand
      return ( SomeRootIsolationStep @algo step, res )