brush-strokes-0.1.0.0: src/lib/Math/Root/Isolation/Newton/GaussSeidel.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- | The Gauss–Seidel method for solving systems of
-- interval linear equations
module Math.Root.Isolation.Newton.GaussSeidel
( -- * Gauss–Seidel step
gaussSeidelUpdate
-- ** Options for the Gauss–Seidel method
, GaussSeidelOptions(..)
, Preconditioner(..), GaussSeidelUpdateMethod(..)
, defaultGaussSeidelOptions
, precondition
)
where
-- base
import Prelude hiding ( unzip )
import Data.Coerce
( coerce )
import Data.Kind
( Type )
import Data.Foldable
( toList )
import Data.Proxy
( Proxy(..) )
import Data.Type.Ord
( OrderingI(..) )
import GHC.TypeNats
( Nat, KnownNat, type (<=), cmpNat )
-- eigen
import qualified Eigen.Matrix as Eigen
( inverse )
-- brush-strokes
import Math.Interval
import Math.Linear
import Math.Linear.Solve
( fromEigen, toEigen )
import Math.Root.Isolation.Core
import Math.Root.Isolation.Utils
--------------------------------------------------------------------------------
-- Gauss–Seidel
-- | Options for the Gauss–Seidel method.
type GaussSeidelOptions :: Nat -> Nat -> Type
data GaussSeidelOptions n d =
GaussSeidelOptions
{ -- | Which preconditioner to use?
gsPreconditioner :: !Preconditioner
-- | Function that projects over the equations we will consider
-- (the identity for a well-determined problem, or a projection for
-- an overdetermined system).
, gsPickEqs :: !( 𝕀ℝ d -> 𝕀ℝ n )
-- | Whether to use a partial or a complete Gauss–Seidel update
, gsUpdate :: !GaussSeidelUpdateMethod
}
instance Show ( GaussSeidelOptions n d ) where
show ( GaussSeidelOptions { gsPreconditioner, gsUpdate } ) =
unlines
[ "GaussSeidelOptions"
, " preconditioner: " ++ show gsPreconditioner
, " update: " ++ show gsUpdate
]
-- | Whether to use a partial or a complete Gauss–Seidel update.
data GaussSeidelUpdateMethod
= GS_Partial
| GS_Complete
deriving stock Show
-- | Default options for the Gauss–Seidel step.
defaultGaussSeidelOptions
:: forall n d
. ( KnownNat n, KnownNat d
, 1 <= n, 1 <= d, n <= d
, Representable Double ( ℝ n ), Representable 𝕀 ( 𝕀ℝ n )
, Representable Double ( ℝ d ), Representable 𝕀 ( 𝕀ℝ d )
)
=> BoxHistory n
-> GaussSeidelOptions n d
defaultGaussSeidelOptions history =
GaussSeidelOptions
{ gsPreconditioner = InverseMidpoint
, gsPickEqs =
case cmpNat @n @d Proxy Proxy of
EQI -> id
LTI ->
-- If there are more equations (d) than variables (n),
-- pick a size n subset of the variables
-- (go through all combinations cyclically).
let choices :: [ Vec n ( Fin d ) ]
choices = choose @d @n
choice :: Vec n ( Fin d )
choice = choices !! ( length history `mod` length choices )
in \ u -> tabulate \ i -> index u ( choice ! i )
, gsUpdate = GS_Complete
}
{-# INLINEABLE defaultGaussSeidelOptions #-}
-- | Preconditioner to use with the interval Gauss–Seidel method.
data Preconditioner
= NoPreconditioning
| InverseMidpoint
deriving stock Show
-- | A partial or complete Gauss–Seidel step for the equation \( A X = B \),
-- refining the initial guess box for \( X \) into up to \( 2^n \) (disjoint) new boxes.
gaussSeidelUpdate
:: forall n
. ( Representable Double ( ℝ n )
, Representable 𝕀 ( 𝕀ℝ n )
, Eq ( ℝ n )
, Show ( 𝕀ℝ n )
)
=> GaussSeidelUpdateMethod -- ^ which step method to use
-> Vec n ( 𝕀ℝ n ) -- ^ columns of \( A \)
-> 𝕀ℝ n -- ^ \( B \)
-> T ( 𝕀ℝ n ) -- ^ initial box \( X \)
-> [ ( T ( 𝕀ℝ n ), Bool ) ]
gaussSeidelUpdate upd as b x =
case upd of
GS_Partial -> gaussSeidelStep as b x
GS_Complete -> gaussSeidelStep_Complete as b x
{-# INLINEABLE gaussSeidelUpdate #-}
-- | Take one interval Gauss–Seidel step for the equation \( A X = B \),
-- refining the initial guess box for \( X \) into up to \( 2^n \) (disjoint) new boxes.
--
-- The boolean indicates whether the Gauss–Seidel step was a contraction.
gaussSeidelStep
:: forall n
. ( Representable Double ( ℝ n )
, Representable 𝕀 ( 𝕀ℝ n )
, Eq ( ℝ n )
)
=> Vec n ( 𝕀ℝ n ) -- ^ columns of \( A \)
-> 𝕀ℝ n -- ^ \( B \)
-> T ( 𝕀ℝ n ) -- ^ initial box \( X \)
-> [ ( T ( 𝕀ℝ n ), Bool ) ]
gaussSeidelStep as b ( T x0 ) = coerce $
forEachCoord @n ( x0, True ) $ \ i ( x, contraction ) -> do
-- For each i, we have an equation: sum_j a_ij * x_j = b_i
--
-- Re-arrange this with x_i on the left to get an iteration:
-- x_i' = ( b_i - sum { j /= i } a_ij * x_j ) / a_ii
--
-- We perform each iteration in turn (for i = 1, ..., n),
-- **using the latest updated value of each x_j** in each iteration.
let s = b `index` i - sum [ ( as ! j ) `index` i * x `index` j
| j <- toList ( universe @n ), j /= i ]
x_i = x `index` i
a_ii = ( as ! i ) `index` i
-- Take a shortcut before performing the division if possible.
if | not $ 0 ∈ ( s - a_ii * x_i )
-- No solutions: don't bother performing a division.
-> [ ]
| 0 ∈ s && 0 ∈ a_ii
-- The division would produce [-oo,+oo]: don't do anything.
-> [ ( x, False ) ]
-- Otherwise, perform the division.
| otherwise
-> do
x_i'0 <- s ⊘ a_ii
( x_i', sub_i ) <- x_i'0 ∩ x_i
return $ ( set i x_i' x, sub_i && contraction )
{-# INLINEABLE gaussSeidelStep #-}
-- | The complete interval-union Gauss–Seidel step.
--
-- Algorithm 2 from:
-- "Using interval unions to solve linear systems of equations with uncertainties"
-- (Montanher, Domes, Schichl, Neumaier) (2017)
gaussSeidelStep_Complete
:: forall n
. ( Representable Double ( ℝ n )
, Representable 𝕀 ( 𝕀ℝ n )
, Show ( 𝕀ℝ n )
)
=> Vec n ( 𝕀ℝ n ) -- ^ columns of \( A \)
-> 𝕀ℝ n -- ^ \( B \)
-> T ( 𝕀ℝ n ) -- ^ initial box \( X \)
-> [ ( T ( 𝕀ℝ n ), Bool ) ]
gaussSeidelStep_Complete as b ( T x0 ) = coerce $ do
( x', subs ) <-
forEachCoord @n ( x0, pure False ) $ \ i ( x, contractions ) -> do
let s = b `index` i - sum [ ( as ! k ) `index` i * x `index` k
| k <- toList ( universe @n ) ]
( x', subs ) <- fromComponents \ j -> do
let x_j = x `index` j
a_ij = ( as ! j ) `index` i
s_j = s ⊖ ( -a_ij * x_j )
-- NB: the paper is missing the minus sign here.
-- In the definition of s we subtract all terms including
-- the a_ij * x_j term, so we need to add it back.
-- Shortcut division if possible (see gaussSeidelStep for commentary).
-- NB: there is a typo in the paper here, it should NOT be
--
-- 0 ∈ ( s_j - a_ii * x_j )
--
-- because we are dividing by a_ij, not a_ii.
if | not $ 0 ∈ ( s_j - a_ij * x_j )
-> [ ]
| 0 ∈ s_j && 0 ∈ a_ij
-> [ ( x_j, False ) ]
| otherwise
-> do
x_j'0 <- s_j ⊘ a_ij
( x_j', sub_j ) <- x_j'0 ∩ x_j
return $ ( x_j', sub_j )
return ( x', (||) <$> contractions <*> subs )
return ( x', and subs )
{-# INLINEABLE gaussSeidelStep_Complete #-}
-- | Pre-condition the system \( AX = B \).
precondition
:: forall n
. ( KnownNat n
, Representable Double ( ℝ n )
, Representable 𝕀 ( 𝕀ℝ n )
)
=> Preconditioner -- ^ pre-conditioning method to use
-> Vec n ( 𝕀ℝ n ) -- ^ columns of \( A \)
-> 𝕀ℝ n -- ^ \( B \)
-> ( Vec n ( 𝕀ℝ n ), 𝕀ℝ n )
precondition meth as b =
case meth of
NoPreconditioning
-> ( as, b )
InverseMidpoint
| let mat = toEigen $ fmap boxMidpoint as
, let precond = Eigen.inverse mat
doPrecond = matMulVec ( fromEigen precond )
-- TODO: avoid this when condition number is small?
-> ( fmap doPrecond as, doPrecond b )
{-# INLINEABLE precondition #-}