{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
--------------------------------------------------------------------------------
-- |
-- Module : LevMar.Intermediate
-- Copyright : (c) 2009 Roel van Dijk & Bas van Dijk
-- License : BSD-style (see the file LICENSE)
--
-- Maintainer : vandijk.roel@gmail.com, v.dijk.bas@gmail.com
-- Stability : Experimental
--
--
--
-- For additional documentation see the documentation of the levmar C
-- library which this library is based on:
--
--
--------------------------------------------------------------------------------
module LevMar.Intermediate
( -- * Model & Jacobian.
Model
, Jacobian
-- * Levenberg-Marquardt algorithm.
, LevMarable
, levmar
, LinearConstraints
-- * Minimization options.
, Options(..)
, defaultOpts
-- * Output
, Info(..)
, StopReason(..)
, CovarMatrix
, LevMarError(..)
) where
import Foreign.Marshal.Array ( allocaArray, peekArray, pokeArray, withArray )
import Foreign.Ptr ( Ptr, nullPtr, plusPtr )
import Foreign.Storable ( Storable )
import Foreign.C.Types ( CInt )
import System.IO.Unsafe ( unsafePerformIO )
import Data.Maybe ( fromJust, fromMaybe, isJust )
import Control.Monad.Instances -- for 'instance Functor (Either a)'
import qualified Bindings.LevMar.CurryFriendly as LMA_C
--------------------------------------------------------------------------------
-- Model & Jacobian.
--------------------------------------------------------------------------------
{- | A functional relation describing measurements represented as a function
from a list of parameters to a list of expected measurements.
* Ensure that the length of the parameters list equals the length of the
initial parameters list in 'levmar'.
* Ensure that the length of the ouput list equals the length of the samples
list in 'levmar'.
For example:
@
hatfldc :: Model Double
hatfldc [p0, p1, p2, p3] = [ p0 - 1.0
, p0 - sqrt p1
, p1 - sqrt p2
, p3 - 1.0
]
@
-}
type Model r = [r] -> [r]
{- | The jacobian of the 'Model' function. Expressed as a function from a list
of parameters to a list of lists which for each expected measurement describes
the partial derivatives of the parameters.
See:
* Ensure that the length of the parameter list equals the length of the initial
parameter list in 'levmar'.
* Ensure that the output matrix has the dimension @n@/x/@m@ where @n@ is the
number of samples and @m@ is the number of parameters.
For example the jacobian of the above @hatfldc@ model is:
@
hatfldc_jac :: Jacobian Double
hatfldc_jac _ p1 p2 _ = [ [1.0, 0.0, 0.0, 0.0]
, [1.0, -0.5 / sqrt p1, 0.0, 0.0]
, [0.0, 1.0, -0.5 / sqrt p2, 0.0]
, [0.0, 0.0, 0.0, 1.0]
]
@
-}
type Jacobian r = [r] -> [[r]]
--------------------------------------------------------------------------------
-- Levenberg-Marquardt algorithm.
--------------------------------------------------------------------------------
-- | The Levenberg-Marquardt algorithm is overloaded to work on 'Double' and 'Float'.
class LevMarable r where
-- | The Levenberg-Marquardt algorithm.
levmar :: Model r -- ^ Model
-> Maybe (Jacobian r) -- ^ Optional jacobian
-> [r] -- ^ Initial parameters
-> [r] -- ^ Samples
-> Integer -- ^ Maximum iterations
-> Options r -- ^ Minimization options
-> Maybe [r] -- ^ Optional lower bounds
-> Maybe [r] -- ^ Optional upper bounds
-> Maybe (LinearConstraints r) -- ^ Optional linear constraints
-> Maybe [r] -- ^ Optional weights
-> Either LevMarError ([r], Info r, CovarMatrix r)
instance LevMarable Float where
levmar = gen_levmar LMA_C.slevmar_der
LMA_C.slevmar_dif
LMA_C.slevmar_bc_der
LMA_C.slevmar_bc_dif
LMA_C.slevmar_lec_der
LMA_C.slevmar_lec_dif
LMA_C.slevmar_blec_der
LMA_C.slevmar_blec_dif
instance LevMarable Double where
levmar = gen_levmar LMA_C.dlevmar_der
LMA_C.dlevmar_dif
LMA_C.dlevmar_bc_der
LMA_C.dlevmar_bc_dif
LMA_C.dlevmar_lec_der
LMA_C.dlevmar_lec_dif
LMA_C.dlevmar_blec_der
LMA_C.dlevmar_blec_dif
{- | @gen_levmar@ takes the low-level C functions as arguments and
executes one of them depending on the optional jacobian and constraints.
Preconditions:
length ys >= length ps
isJust mLowBs && length (fromJust mLowBs) == length ps
&& isJust mUpBs && length (fromJust mUpBs) == length ps
boxConstrained && (all $ zipWith (<=) (fromJust mLowBs) (fromJust mUpBs))
-}
gen_levmar :: forall cr r. (Storable cr, RealFrac cr, Real r, Fractional r)
=> LMA_C.LevMarDer cr
-> LMA_C.LevMarDif cr
-> LMA_C.LevMarBCDer cr
-> LMA_C.LevMarBCDif cr
-> LMA_C.LevMarLecDer cr
-> LMA_C.LevMarLecDif cr
-> LMA_C.LevMarBLecDer cr
-> LMA_C.LevMarBLecDif cr
-> Model r -- ^ Model
-> Maybe (Jacobian r) -- ^ Optional jacobian
-> [r] -- ^ Initial parameters
-> [r] -- ^ Samples
-> Integer -- ^ Maximum iterations
-> Options r -- ^ Options
-> Maybe [r] -- ^ Optional lower bounds
-> Maybe [r] -- ^ Optional upper bounds
-> Maybe (LinearConstraints r) -- ^ Optional linear constraints
-> Maybe [r] -- ^ Optional weights
-> Either LevMarError ([r], Info r, CovarMatrix r)
gen_levmar f_der
f_dif
f_bc_der
f_bc_dif
f_lec_der
f_lec_dif
f_blec_der
f_blec_dif
model mJac ps ys itMax opts mLowBs mUpBs mLinC mWeights
= unsafePerformIO .
withArray (map realToFrac ps) $ \psPtr ->
withArray (map realToFrac ys) $ \ysPtr ->
withArray (map realToFrac $ optsToList opts) $ \optsPtr ->
allocaArray LMA_C._LM_INFO_SZ $ \infoPtr ->
allocaArray covarLen $ \covarPtr ->
LMA_C.withModel (convertModel model) $ \modelPtr -> do
let runDif :: LMA_C.LevMarDif cr -> IO CInt
runDif f = f modelPtr
psPtr
ysPtr
(fromIntegral lenPs)
(fromIntegral lenYs)
(fromIntegral itMax)
optsPtr
infoPtr
nullPtr
covarPtr
nullPtr
r <- case mJac of
Just jac -> LMA_C.withJacobian (convertJacobian jac) $ \jacobPtr ->
let runDer :: LMA_C.LevMarDer cr -> IO CInt
runDer f = runDif $ f jacobPtr
in if boxConstrained
then if linConstrained
then withBoxConstraints (withLinConstraints $ withWeights runDer) f_blec_der
else withBoxConstraints runDer f_bc_der
else if linConstrained
then withLinConstraints runDer f_lec_der
else runDer f_der
Nothing -> if boxConstrained
then if linConstrained
then withBoxConstraints (withLinConstraints $ withWeights runDif) f_blec_dif
else withBoxConstraints runDif f_bc_dif
else if linConstrained
then withLinConstraints runDif f_lec_dif
else runDif f_dif
if r < 0
&& r /= LMA_C._LM_ERROR_SINGULAR_MATRIX -- we don't treat these two as an error
&& r /= LMA_C._LM_ERROR_SUM_OF_SQUARES_NOT_FINITE
then return . Left $ convertLevMarError r
else do result <- peekArray lenPs psPtr
info <- peekArray LMA_C._LM_INFO_SZ infoPtr
let covarPtrEnd = plusPtr covarPtr covarLen
let convertCovarMatrix ptr
| ptr == covarPtrEnd = return []
| otherwise = do row <- peekArray lenPs ptr
rows <- convertCovarMatrix $ plusPtr ptr lenPs
return $ row : rows
covar <- convertCovarMatrix covarPtr
return $ Right ( map realToFrac result
, listToInfo info
, map (map realToFrac) covar
)
where
lenPs = length ps
lenYs = length ys
covarLen = lenPs * lenPs
(cMat, rhcVec) = fromJust mLinC
-- Whether the parameters are constrained by a linear equation.
linConstrained = isJust mLinC
-- Whether the parameters are constrained by a bounding box.
boxConstrained = isJust mLowBs || isJust mUpBs
withBoxConstraints f g = maybeWithArray ((fmap . fmap) realToFrac mLowBs) $ \lBsPtr ->
maybeWithArray ((fmap . fmap) realToFrac mUpBs) $ \uBsPtr ->
f $ g lBsPtr uBsPtr
withLinConstraints f g = withArray (map realToFrac $ concat cMat) $ \cMatPtr ->
withArray (map realToFrac rhcVec) $ \rhcVecPtr ->
f . g cMatPtr rhcVecPtr . fromIntegral $ length cMat
withWeights f g = maybeWithArray ((fmap . fmap) realToFrac mWeights) $ f . g
convertModel :: (Real r, Fractional r, Storable c, Real c, Fractional c)
=> Model r -> LMA_C.Model c
convertModel model = \parPtr hxPtr numPar _ _ -> do
params <- peekArray (fromIntegral numPar) parPtr
pokeArray hxPtr . map realToFrac . model $ map realToFrac params
convertJacobian :: (Real r, Fractional r, Storable c, Real c, Fractional c)
=> Jacobian r -> LMA_C.Jacobian c
convertJacobian jac = \parPtr jPtr numPar _ _ -> do
params <- peekArray (fromIntegral numPar) parPtr
pokeArray jPtr . concatMap (map realToFrac) . jac $ map realToFrac params
maybeWithArray :: Storable a => Maybe [a] -> (Ptr a -> IO b) -> IO b
maybeWithArray Nothing f = f nullPtr
maybeWithArray (Just xs) f = withArray xs f
-- | Linear constraints consisting of a constraints matrix, /kxm/ and
-- a right hand constraints vector, /kx1/ where /m/ is the number of
-- parameters and /k/ is the number of constraints.
type LinearConstraints r = ([[r]], [r])
--------------------------------------------------------------------------------
-- Minimization options.
--------------------------------------------------------------------------------
-- | Minimization options
data Options r =
Opts { optScaleInitMu :: r -- ^ Scale factor for initial /mu/.
, optStopNormInfJacTe :: r -- ^ Stopping thresholds for @||J^T e||_inf@.
, optStopNorm2Dp :: r -- ^ Stopping thresholds for @||Dp||_2@.
, optStopNorm2E :: r -- ^ Stopping thresholds for @||e||_2@.
, optDelta :: r -- ^ Step used in the difference approximation to the Jacobian.
-- If @optDelta<0@, the Jacobian is approximated
-- with central differences which are more accurate
-- (but slower!) compared to the forward differences
-- employed by default.
} deriving Show
-- | Default minimization options
defaultOpts :: Fractional r => Options r
defaultOpts = Opts { optScaleInitMu = LMA_C._LM_INIT_MU
, optStopNormInfJacTe = LMA_C._LM_STOP_THRESH
, optStopNorm2Dp = LMA_C._LM_STOP_THRESH
, optStopNorm2E = LMA_C._LM_STOP_THRESH
, optDelta = LMA_C._LM_DIFF_DELTA
}
optsToList :: Options r -> [r]
optsToList (Opts mu eps1 eps2 eps3 delta) =
[mu, eps1, eps2, eps3, delta]
--------------------------------------------------------------------------------
-- Output
--------------------------------------------------------------------------------
-- | Information regarding the minimization.
data Info r = Info { infNorm2initE :: r -- ^ @||e||_2@ at initial parameters.
, infNorm2E :: r -- ^ @||e||_2@ at estimated parameters.
, infNormInfJacTe :: r -- ^ @||J^T e||_inf@ at estimated parameters.
, infNorm2Dp :: r -- ^ @||Dp||_2@ at estimated parameters.
, infMuDivMax :: r -- ^ @\mu/max[J^T J]_ii ]@ at estimated parameters.
, infNumIter :: Integer -- ^ Number of iterations.
, infStopReason :: StopReason -- ^ Reason for terminating.
, infNumFuncEvals :: Integer -- ^ Number of function evaluations.
, infNumJacobEvals :: Integer -- ^ Number of jacobian evaluations.
, infNumLinSysSolved :: Integer -- ^ Number of linear systems solved, i.e. attempts for reducing error.
} deriving Show
listToInfo :: (RealFrac cr, Fractional r) => [cr] -> Info r
listToInfo [a,b,c,d,e,f,g,h,i,j] =
Info { infNorm2initE = realToFrac a
, infNorm2E = realToFrac b
, infNormInfJacTe = realToFrac c
, infNorm2Dp = realToFrac d
, infMuDivMax = realToFrac e
, infNumIter = floor f
, infStopReason = toEnum $ floor g - 1
, infNumFuncEvals = floor h
, infNumJacobEvals = floor i
, infNumLinSysSolved = floor j
}
listToInfo _ = error "liftToInfo: wrong list length"
-- | Reason for terminating.
data StopReason = SmallGradient -- ^ Stopped because of small gradient @J^T e@.
| SmallDp -- ^ Stopped because of small Dp.
| MaxIterations -- ^ Stopped because maximum iterations was reached.
| SingularMatrix -- ^ Stopped because of singular matrix. Restart from current estimated parameters with increased 'optScaleInitMu'.
| SmallestError -- ^ Stopped because no further error reduction is possible. Restart with increased 'optScaleInitMu'.
| SmallNorm2E -- ^ Stopped because of small @||e||_2@.
| InvalidValues -- ^ Stopped because model function returned invalid values (i.e. NaN or Inf). This is a user error.
deriving (Show, Enum)
-- | Covariance matrix corresponding to LS solution.
type CovarMatrix r = [[r]]
--------------------------------------------------------------------------------
-- Error
--------------------------------------------------------------------------------
data LevMarError
= LevMarError -- ^ Generic error (not one of the others)
| LapackError -- ^ A call to a lapack subroutine failed in the underlying C levmar library.
| FailedBoxCheck -- ^ At least one lower bound exceeds the upper one.
| MemoryAllocationFailure -- ^ A call to @malloc@ failed in the underlying C levmar library.
| ConstraintMatrixRowsGtCols -- ^ The matrix of constraints cannot have more rows than columns.
| ConstraintMatrixNotFullRowRank -- ^ Constraints matrix is not of full row rank.
| TooFewMeasurements -- ^ Cannot solve a problem with fewer measurements than unknowns.
-- In case linear constraints are provided, this error is also returned
-- when the number of measurements is smaller than the number of unknowns
-- minus the number of equality constraints.
deriving Show
levmarCErrorToLevMarError :: [(CInt, LevMarError)]
levmarCErrorToLevMarError =
[ (LMA_C._LM_ERROR, LevMarError)
, (LMA_C._LM_ERROR_LAPACK_ERROR, LapackError)
--, (LMA_C._LM_ERROR_NO_JACOBIAN, can never happen)
--, (LMA_C._LM_ERROR_NO_BOX_CONSTRAINTS, can never happen)
, (LMA_C._LM_ERROR_FAILED_BOX_CHECK, FailedBoxCheck)
, (LMA_C._LM_ERROR_MEMORY_ALLOCATION_FAILURE, MemoryAllocationFailure)
, (LMA_C._LM_ERROR_CONSTRAINT_MATRIX_ROWS_GT_COLS, ConstraintMatrixRowsGtCols)
, (LMA_C._LM_ERROR_CONSTRAINT_MATRIX_NOT_FULL_ROW_RANK, ConstraintMatrixNotFullRowRank)
, (LMA_C._LM_ERROR_TOO_FEW_MEASUREMENTS, TooFewMeasurements)
--, (LMA_C._LM_ERROR_SINGULAR_MATRIX, we don't treat this as an error)
--, (LMA_C._LM_ERROR_SUM_OF_SQUARES_NOT_FINITE, we don't treat this as an error)
]
convertLevMarError :: CInt -> LevMarError
convertLevMarError err = fromMaybe (error "Unknown levmar error") $
lookup err levmarCErrorToLevMarError
-- The End ---------------------------------------------------------------------