regression-simple 0.1.1 → 0.2
raw patch · 9 files changed
+2166/−579 lines, 9 filesdep +addep +deepseqdep +math-functionsdep −vectordep ~base
Dependencies added: ad, deepseq, math-functions, regression-simple, semigroups, splitmix, statistics, tasty, tasty-hunit
Dependencies removed: vector
Dependency ranges changed: base
Files
- changelog.md +8/−1
- gnuplot/linear.dat +20/−0
- gnuplot/quad.dat +20/−0
- regression-simple.cabal +58/−8
- src/Math/Regression/Simple.hs +1203/−570
- src/Math/Regression/Simple/LinAlg.hs +402/−0
- src/Numeric/KBN.hs +62/−0
- test/generate-test-data.hs +60/−0
- test/regression-simple-tests.hs +333/−0
changelog.md view
@@ -1,4 +1,11 @@+# 0.2++Large refactoring:++* Add `*WithWeights` variants+* Move linear algebra minilib into own module+* Expose accumulators+ # 0.1.1 * Add `*WithErrors` variants to regression functions.-
+ gnuplot/linear.dat view
@@ -0,0 +1,20 @@+ 1.33343 8.43365 0.20000 1.20000+ 2.46969 11.46688 0.30000 1.40000+ 2.80364 15.59652 0.20000 1.60000+ 4.00046 15.56544 0.30000 1.20000+ 5.18323 20.55600 0.20000 1.40000+ 5.66586 21.16630 0.30000 1.60000+ 6.82541 29.06629 0.20000 1.20000+ 7.64329 33.09135 0.30000 1.40000+ 8.80296 34.41986 0.20000 1.60000+ 9.65792 35.80643 0.30000 1.20000+ 10.63990 39.34492 0.20000 1.40000+ 12.48292 39.14740 0.30000 1.60000+ 12.83448 44.32188 0.20000 1.20000+ 13.93802 45.79094 0.30000 1.40000+ 14.98339 49.75683 0.20000 1.60000+ 16.09785 53.33996 0.30000 1.20000+ 16.74771 55.50356 0.20000 1.40000+ 17.81197 59.54537 0.30000 1.60000+ 18.56691 61.87966 0.20000 1.20000+ 20.64801 65.51835 0.30000 1.40000
+ gnuplot/quad.dat view
@@ -0,0 +1,20 @@+ 1.23669 1.90145 0.20000 1.20000+ 2.11352 -0.19517 0.30000 1.40000+ 2.98278 -1.34899 0.20000 1.60000+ 3.87135 -6.42140 0.30000 1.20000+ 5.50783 -6.20445 0.20000 1.40000+ 6.74626 -9.14974 0.30000 1.60000+ 7.41109 -12.14659 0.20000 1.20000+ 8.44699 -13.34493 0.30000 1.40000+ 9.34861 -14.56375 0.20000 1.60000+ 10.16446 -15.58626 0.30000 1.20000+ 11.17560 -16.32637 0.20000 1.40000+ 11.85809 -17.63580 0.30000 1.60000+ 12.88762 -17.86456 0.20000 1.20000+ 14.05061 -17.48367 0.30000 1.40000+ 15.15438 -18.61585 0.20000 1.60000+ 16.42014 -17.24635 0.30000 1.20000+ 17.00652 -17.96829 0.20000 1.40000+ 18.07526 -12.69336 0.30000 1.60000+ 18.63128 -18.25398 0.20000 1.20000+ 20.70280 -12.68441 0.30000 1.40000
regression-simple.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: regression-simple-version: 0.1.1+version: 0.2 synopsis: Simple linear and quadratic regression category: Math description:@@ -19,9 +19,15 @@ maintainer: Oleg Grenrus <oleg.grenrus@iki.fi> homepage: https://github.com/phadej/regression-simple bug-reports: https://github.com/phadej/regression-simple/issues-extra-source-files: changelog.md+extra-source-files:+ changelog.md+ gnuplot/linear.dat+ gnuplot/quad.dat+ tested-with:- GHC ==7.4.2+ GHC ==7.0.4+ || ==7.2.2+ || ==7.4.2 || ==7.6.3 || ==7.8.4 || ==7.10.3@@ -32,16 +38,60 @@ || ==8.8.4 || ==8.10.7 || ==9.0.2- || ==9.2.1+ || ==9.2.5+ || ==9.4.4 source-repository head type: git location: https://github.com/phadej/regression-simple library+ default-language: Haskell2010+ ghc-options: -Wall+ hs-source-dirs: src+ exposed-modules:+ Math.Regression.Simple+ Math.Regression.Simple.LinAlg+ Numeric.KBN++ build-depends:+ , base >=4.3 && <4.18+ , deepseq++ if !impl(ghc >=8.0)+ build-depends: semigroups >=0.18.5 && <0.21++ x-docspec-extra-packages: math-functions statistics ad++test-suite generate-test-data default-language: Haskell2010- hs-source-dirs: src- exposed-modules: Math.Regression.Simple+ ghc-options: -Wall+ hs-source-dirs: test+ type: exitcode-stdio-1.0+ main-is: generate-test-data.hs build-depends:- , base >=4.5 && <4.17- , vector ^>=0.12.0.0+ , base+ , splitmix ^>=0.1.0.4++test-suite regression-simple-tests+ default-language: Haskell2010+ ghc-options: -Wall+ hs-source-dirs: test+ type: exitcode-stdio-1.0+ main-is: regression-simple-tests.hs+ build-depends:+ , base+ , regression-simple++ build-depends:+ , tasty ^>=1.4.0.1+ , tasty-hunit ^>=0.10.0.3++ if impl(ghc >=7.4)+ build-depends:+ , ad >=4.4.1 && <4.6+ , math-functions ^>=0.3.4.2+ , statistics ^>=0.10.2.0 || ^>=0.15.2.0 || ^>=0.16.0.1++ if !impl(ghc >=8.0)+ build-depends: semigroups
src/Math/Regression/Simple.hs view
@@ -2,573 +2,1206 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs #-}-module Math.Regression.Simple (- -- * Regressions- linear,- linearWithErrors,- quadratic,- quadraticWithErrors,- quadraticAndLinear,- quadraticAndLinearWithErrors,- -- * Operations- Add (..),- Eye (..),- Mult (..),- Det (..),- Inv (..),- -- * Zeros- zerosLin,- zerosQuad,- optimaQuad,- -- * Two dimensions- V2 (..),- M22 (..),- -- * Three dimensions- V3 (..),- M33 (..),- -- * Auxiliary classes- Foldable' (..),- IsDoublePair (..),- ) where--import Data.Complex (Complex (..))--import qualified Data.List as L-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as U---- $setup--- >>> :set -XTypeApplications------ >>> import Numeric (showFFloat)------ Don't show too much decimal digits------ >>> showDouble x = showFFloat @Double (Just (min 5 (5 - ceiling (logBase 10 x)))) x--- >>> showDouble 123.456 ""--- "123.46"------ >>> showDouble 1234567 ""--- "1234567"------ >>> showDouble 123.4567890123456789 ""--- "123.46"------ >>> showDouble 0.0000000000000012345 ""--- "0.00000"--- --- >>> newtype PP a = PP a--- >>> class Show' a where showsPrec' :: Int -> a -> ShowS--- >>> instance Show' a => Show (PP a) where showsPrec d (PP x) = showsPrec' d x--- >>> instance Show' Double where showsPrec' d x = if x < 0 then showParen (d > 6) (showChar '-' . showDouble (negate x)) else showDouble x--- >>> instance (Show' a, Show' b) => Show' (a, b) where showsPrec' d (x, y) = showParen True $ showsPrec' 0 x . showString ", " . showsPrec' 0 y--- >>> instance Show' V2 where showsPrec' d (V2 x y) = showParen (d > 10) $ showString "V2 " . showsPrec' 11 x . showChar ' ' . showsPrec' 11 y--- >>> instance Show' V3 where showsPrec' d (V3 x y z) = showParen (d > 10) $ showString "V3 " . showsPrec' 11 x . showChar ' ' . showsPrec' 11 y . showChar ' ' . showsPrec' 11 z------ Inputs:--- >>> let input1 = [(0, 1), (1, 3), (2, 5)]--- >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]--- >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]--------------------------------------------------------------------------------------- Classes------------------------------------------------------------------------------------ | Addition-class Add a where- zero :: a- add :: a -> a -> a---- | Identity-class Eye a where- eye :: a---- | Multiplication of different things.-class Eye a => Mult a b c | a b -> c where- mult :: a -> b -> c---- | Determinant-class Eye a => Det a where- det :: a -> Double---- | Inverse-class Det a => Inv a where- inv :: a -> a--infixl 6 `add`-infixl 7 `mult`--instance Eye Double where- eye = 1--instance Add Double where- zero = 0- add = (+)--instance Det Double where- det = id--instance Inv Double where- inv = recip------------------------------------------------------------------------------------ Zeros------------------------------------------------------------------------------------ | Solve linear equation.------ >>> zerosLin (V2 1 2)--- -2.0----zerosLin :: V2 -> Double-zerosLin (V2 a b) = negate (b / a)---- | Solve quadratic equation.------ >>> zerosQuad (V3 2 0 (-1))--- Right (-0.7071067811865476,0.7071067811865476)------ >>> zerosQuad (V3 2 0 1)--- Left ((-0.0) :+ (-0.7071067811865476),(-0.0) :+ 0.7071067811865476)------ Double root is not treated separately:------ >>> zerosQuad (V3 1 0 0)--- Right (-0.0,0.0)------ >>> zerosQuad (V3 1 (-2) 1)--- Right (1.0,1.0)----zerosQuad :: V3 -> Either (Complex Double, Complex Double) (Double, Double)-zerosQuad (V3 a b c)- | delta < 0 = Left ((-b/da) :+ (-sqrtNDelta/da), (-b/da) :+ (sqrtNDelta/da))- | otherwise = Right ((- b - sqrtDelta) / da, (-b + sqrtDelta) / da)- where- delta = b*b - 4 * a * c- sqrtDelta = sqrt delta- sqrtNDelta = sqrt (- delta)- da = 2 * a---- | Find an optima point.------ >>> optimaQuad (V3 1 (-2) 0)--- 1.0------ compare to------ >>> zerosQuad (V3 1 (-2) 0)--- Right (0.0,2.0)----optimaQuad :: V3 -> Double-optimaQuad (V3 a b _) = zerosLin (V2 (2 * a) b)------------------------------------------------------------------------------------ 2 dimensions------------------------------------------------------------------------------------ | 2d vector. Strict pair of 'Double's.------ Also used to represent linear polynomial: @V2 a b@ \(= a x + b\).----data V2 = V2 !Double !Double- deriving (Eq, Show)--instance Add V2 where- zero = V2 0 0- add (V2 x y) (V2 x' y') = V2 (x + x') (y + y')- {-# INLINE zero #-}- {-# INLINE add #-}--instance Mult Double V2 V2 where- mult k (V2 x y) = V2 (k * x) (k * y)- {-# INLINE mult #-}---- | 2×2 matrix.-data M22 = M22 !Double !Double !Double !Double- deriving (Eq, Show)--instance Add M22 where- zero = M22 0 0 0 0- add (M22 a b c d) (M22 a' b' c' d') = M22 (a + a') (b + b') (c + c') (d + d')- {-# INLINE zero #-}- {-# INLINE add #-}--instance Eye M22 where- eye = M22 1 0 0 1- {-# INLINE eye #-}--instance Det M22 where det = det2-instance Inv M22 where inv = inv2--instance Mult Double M22 M22 where- mult k (M22 a b c d) = M22 (k * a) (k * b) (k * c) (k * d)- {-# INLINE mult #-}--instance Mult M22 V2 V2 where- mult (M22 a b c d) (V2 u v) = V2 (a * u + b * v) (c * u + d * v)- {-# INLINE mult #-}---- | >>> M22 1 2 3 4 `mult` eye @M22--- M22 1.0 2.0 3.0 4.0-instance Mult M22 M22 M22 where- mult (M22 a b c d) (M22 x y z w) = M22- (a * x + b * z) (a * y + b * w)- (c * x + d * z) (c * y + d * w)- {-# INLINE mult #-}--det2 :: M22 -> Double-det2 (M22 a b c d) = a * d - b * c-{-# INLINE det2 #-}--inv2 :: M22 -> M22-inv2 m@(M22 a b c d) = M22- ( d / det) (- b / det)- (- c / det) ( a / det)- where- det = det2 m-{-# INLINE inv2 #-}------------------------------------------------------------------------------------ 3 dimensions------------------------------------------------------------------------------------ | 3d vector. Strict triple of 'Double's.------ Also used to represent quadratic polynomial: @V3 a b c@ \(= a x^2 + b x + c\).-data V3 = V3 !Double !Double !Double- deriving (Eq, Show)--instance Add V3 where- zero = V3 0 0 0- add (V3 x y z) (V3 x' y' z') = V3 (x + x') (y + y') (z + z')- {-# INLINE zero #-}- {-# INLINE add #-}--instance Mult Double V3 V3 where- mult k (V3 x y z) = V3 (k * x) (k * y) (k * z)- {-# INLINE mult #-}---- | 3×3 matrix.-data M33 = M33- !Double !Double !Double- !Double !Double !Double- !Double !Double !Double- deriving (Eq, Show)--instance Add M33 where- zero = M33 0 0 0 0 0 0 0 0 0-- add (M33 a b c d e f g h i) (M33 a' b' c' d' e' f' g' h' i') = M33- (a + a') (b + b') (c + c')- (d + d') (e + e') (f + f')- (g + g') (h + h') (i + i')- {-# INLINE zero #-}- {-# INLINE add #-}--instance Eye M33 where- eye = M33 1 0 0 0 1 0 0 0 1- {-# INLINE eye #-}--instance Det M33 where det = det3-instance Inv M33 where inv = inv3--instance Mult Double M33 M33 where- mult k (M33 a b c d e f g h i) = M33- (k * a) (k * b) (k * c)- (k * d) (k * e) (k * f)- (k * g) (k * h) (k * i)- {-# INLINE mult #-}--instance Mult M33 V3 V3 where- mult (M33 a b c- d e f- g h i) (V3 u v w) = V3- (a * u + b * v + c * w)- (d * u + e * v + f * w)- (g * u + h * v + i * w)- {-# INLINE mult #-}---- TODO: instance Mult M33 M33 M33 where--det3 :: M33 -> Double-det3 (M33 a b c- d e f- g h i)- = a * (e*i-f*h) - d * (b*i-c*h) + g * (b*f-c*e)-{-# INLINE det3 #-}--inv3 :: M33 -> M33-inv3 m@(M33 a b c- d e f- g h i)- = M33 a' b' c'- d' e' f'- g' h' i'- where- a' = cofactor e f h i / det- b' = cofactor c b i h / det- c' = cofactor b c e f / det- d' = cofactor f d i g / det- e' = cofactor a c g i / det- f' = cofactor c a f d / det- g' = cofactor d e g h / det- h' = cofactor b a h g / det- i' = cofactor a b d e / det- cofactor q r s t = det2 (M22 q r s t)- det = det3 m-{-# INLINE inv3 #-}------------------------------------------------------------------------------------ Regressions------------------------------------------------------------------------------------ | Linear regression.------ The type is------ @--- 'linear' :: [('Double', 'Double')] -> 'V2'--- @------ but overloaded to work with boxed and unboxed 'Vector's.------ >>> let input1 = [(0, 1), (1, 3), (2, 5)]--- >>> PP $ linear input1--- V2 2.0000 1.00000------ >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]--- >>> PP $ linear input2--- V2 2.0063 0.88685----linear :: (Foldable' xs x, IsDoublePair x) => xs -> V2-linear = fst . linearWithErrors---- | Like 'linear' but also return parameters' standard errors.------ To get confidence intervals you should multiply the errors--- by @quantile (studentT (n - 2)) ci'@ from @statistics@ package--- or similar.--- For big @n@ using value 1 gives 68% interval and using value 2 gives 95% confidence interval.--- See https://en.wikipedia.org/wiki/Student%27s_t-distribution#Table_of_selected_values--- (@quantile@ calculates one-sided values, you need two-sided, thus adjust @ci@ value).------ The first input is perfect fit:------ >>> PP $ linearWithErrors input1--- (V2 2.0000 1.00000, V2 0.00000 0.00000)------ The second input is quite good:------ >>> PP $ linearWithErrors input2--- (V2 2.0063 0.88685, V2 0.09550 0.23826)------ But the third input isn't so much,--- standard error of a slope argument is 20%.------ >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]--- >>> PP $ linearWithErrors input3--- (V2 3.0000 1.00000, V2 0.63246 1.1832)------ @since 0.1.1----linearWithErrors :: (Foldable' xs x, IsDoublePair x) => xs -> (V2, V2)-linearWithErrors = linearImpl . kahan2--linearImpl :: Kahan2 -> (V2, V2)-linearImpl (K2 n' (V2 x _) (V2 x2 _) (V2 y _) (V2 y2 _) (V2 xy _)) =- (params, errors)- where- n :: Double- n = fromIntegral n'-- matrix@(M22 a11 _ _ a22) = inv2 (M22 x2 x x n)- params@(V2 a b) = mult matrix (V2 xy y)-- errors = V2 sa sb-- -- ensure that error is always non-negative.- -- Due rounding errors, in perfect fit situations it can be slightly negative.- err = max 0 (y2 - a * xy - b * y)- sa = sqrt (a11 * err / (n - 2))- sb = sqrt (a22 * err / (n - 2))---- | Quadratic regression.------ The type is------ @--- 'quadratic' :: [('Double', 'Double')] -> 'V3'--- @------ but overloaded to work with boxed and unboxed 'Vector's.------ >>> let input1 = [(0, 1), (1, 3), (2, 5)]--- >>> quadratic input1--- V3 0.0 2.0 1.0------ >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]--- >>> PP $ quadratic input2--- V3 (-0.00589) 2.0313 0.87155------ >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]--- >>> PP $ quadratic input3--- V3 1.00000 0.00000 2.0000----quadratic :: (Foldable' xs x, IsDoublePair x) => xs -> V3-quadratic data_ = mult (inv3 (M33 x2 x n x3 x2 x x4 x3 x2)) (V3 y xy x2y)- where- K3 n' (V2 x _) (V2 x2 _) (V2 x3 _) (V2 x4 _) (V2 y _) (V2 xy _) (V2 x2y _) (V2 _y2 _) = kahan3 data_-- n :: Double- n = fromIntegral n'---- | Like 'quadratic' but also return parameters' standard errors.------ >>> PP $ quadraticWithErrors input2--- (V3 (-0.00589) 2.0313 0.87155, V3 0.09281 0.41070 0.37841)------ >>> PP $ quadraticWithErrors input3--- (V3 1.00000 0.00000 2.0000, V3 0.00000 0.00000 0.00000)------ @since 0.1.1----quadraticWithErrors :: (Foldable' xs x, IsDoublePair x) => xs -> (V3, V3)-quadraticWithErrors = quadraticImpl . kahan3--quadraticImpl :: Kahan3 -> (V3, V3)-quadraticImpl (K3 n' (V2 x _) (V2 x2 _) (V2 x3 _) (V2 x4 _) (V2 y _) (V2 xy _) (V2 x2y _) (V2 y2 _)) =- (params, errors)- where- n :: Double- n = fromIntegral n'-- matrix@(M33 a11 _ _- _ a22 _- _ _ a33) = inv3 (M33 x4 x3 x2 x3 x2 x x2 x n)-- params@(V3 a b c) = mult matrix (V3 x2y xy y)-- errors = V3 sa sb sc-- err = max 0 (y2 - a * x2y - b * xy - c * y)- sa = sqrt (a11 * err / (n - 3))- sb = sqrt (a22 * err / (n - 3))- sc = sqrt (a33 * err / (n - 3))---- | Do both linear and quadratic regression in one data scan.------ >>> PP $ quadraticAndLinear input2--- (V3 (-0.00589) 2.0313 0.87155, V2 2.0063 0.88685)----quadraticAndLinear :: (Foldable' xs x, IsDoublePair x) => xs -> (V3, V2)-quadraticAndLinear = fst . quadraticAndLinearWithErrors---- | Like 'quadraticAndLinear' but also return parameters' standard errors------ >>> PP $ quadraticAndLinearWithErrors input2--- ((V3 (-0.00589) 2.0313 0.87155, V2 2.0063 0.88685), (V3 0.09281 0.41070 0.37841, V2 0.09550 0.23826))------ @since 0.1.1----quadraticAndLinearWithErrors :: (Foldable' xs x, IsDoublePair x) => xs -> ((V3, V2), (V3, V2))-quadraticAndLinearWithErrors data_ =- ((paramsQ, paramsL), (errorsQ, errorsL))- where- k3@(K3 n x x2 x3 x4 y xy x2y y2) = kahan3 data_- k2 = K2 n x x2 y y2 xy-- (paramsL, errorsL) = linearImpl k2- (paramsQ, errorsQ) = quadraticImpl k3------------------------------------------------------------------------------------ Input------------------------------------------------------------------------------------ | Like 'Foldable' but with element in the class definition.-class Foldable' xs x | xs -> x where- foldl' :: (b -> x -> b) -> b -> xs -> b--instance Foldable' [a] a where foldl' = L.foldl'-instance Foldable' (V.Vector a) a where foldl' = V.foldl'-instance U.Unbox a => Foldable' (U.Vector a) a where foldl' = U.foldl'---- | Class witnessing that @dp@ has a pair of 'Double's.-class IsDoublePair dp where- withDP :: dp -> (Double -> Double -> r) -> r- makeDP :: Double -> Double -> dp--instance IsDoublePair V2 where- withDP (V2 x y) k = k x y- makeDP = V2--instance (a ~ Double, b ~ Double) => IsDoublePair (a, b) where- withDP ~(x, y) k = k x y- makeDP = (,)------------------------------------------------------------------------------------ Kahan2----------------------------------------------------------------------------------data Kahan2 = K2- { k2n :: {-# UNPACK #-} !Int- , k2x :: {-# UNPACK #-} !V2- , k2x2 :: {-# UNPACK #-} !V2- , k2y :: {-# UNPACK #-} !V2- , k2y2 :: {-# UNPACK #-} !V2- , k2xy :: {-# UNPACK #-} !V2- }--zeroKahan2 :: Kahan2-zeroKahan2 = K2 0 zero zero zero zero zero---- | https://en.wikipedia.org/wiki/Kahan_summation_algorithm-addKahan :: V2 -> Double -> V2-addKahan (V2 acc c) i =- let y = i - c- t = acc + y- in V2 t ((t - acc) - y)--kahan2 :: (Foldable' xs x, IsDoublePair x) => xs -> Kahan2-kahan2 = foldl' f zeroKahan2 where- f (K2 n x x2 y y2 xy) uv = withDP uv $ \u v -> K2- (succ n)- (addKahan x u)- (addKahan x2 (u * u))- (addKahan y v)- (addKahan y2 (v * v))- (addKahan xy (u * v))------------------------------------------------------------------------------------ Kahan3----------------------------------------------------------------------------------data Kahan3 = K3- { k3n :: {-# UNPACK #-} !Int- , k3x :: {-# UNPACK #-} !V2- , k3x2 :: {-# UNPACK #-} !V2- , k3x3 :: {-# UNPACK #-} !V2- , k3x4 :: {-# UNPACK #-} !V2- , k3y :: {-# UNPACK #-} !V2- , k3xy :: {-# UNPACK #-} !V2- , k3x2y :: {-# UNPACK #-} !V2- , ky2 :: {-# UNPACK #-} !V2- }--zeroKahan3 :: Kahan3-zeroKahan3 = K3 0 zero zero zero zero zero zero zero zero--kahan3 :: (Foldable' xs x, IsDoublePair x) => xs -> Kahan3-kahan3 = foldl' f zeroKahan3 where- f (K3 n x x2 x3 x4 y xy x2y y2) uv = withDP uv $ \u v ->- let u2 = u * u- in K3- (succ n)- (addKahan x u)- (addKahan x2 u2)- (addKahan x3 (u * u2))- (addKahan x4 (u2 * u2))- (addKahan y v)- (addKahan xy (u * v))- (addKahan x2y (u2 * v))- (addKahan y2 (v * v))+{-# LANGUAGE RecordWildCards #-}+-- |+--+-- @regression-simple@ provides (hopefully) simple regression functions.+--+-- The @'linear' :: Foldable f => (a -> (Double, Double)) -> f a -> 'V2'@+-- is the simplest one.+--+-- There are variants with weights, y-errors, and x and y-errors.+-- In addition, package includes Levenberg–Marquardt algorithm implementation+-- to fit arbitrary functions (with one, two or three parameters),+-- as long as you can give their partial derivatives as well (@ad@ package is handy for that).+--+-- For multiple independent variable ordinary least squares+-- or Levenberg-Marquard with functions with \> 3 parameter you should look elsewhere.+--+-- Package has been tested to return similar results as @fit@ functionality in @gnuplot@+-- (L-M doesn't always converge to exactly the same points in parameter space).+--+module Math.Regression.Simple (+ -- * Linear regression+ linear,+ linearFit,+ linearWithWeights,+ linearWithYerrors,+ linearWithXYerrors,+ -- ** Step-by-step interface+ linearFit',+ LinRegAcc (..),+ zeroLinRegAcc,+ addLinReg,+ addLinRegW,+ -- * Quadratic regression+ quadratic,+ quadraticFit,+ quadraticWithWeights,+ quadraticWithYerrors,+ quadraticWithXYerrors,+ -- ** Step-by-step interface+ quadraticFit',+ QuadRegAcc (..),+ zeroQuadRegAcc,+ addQuadReg,+ addQuadRegW,+ quadRegAccToLin,+ -- * Levenberg–Marquardt algorithm+ -- ** One parameter+ levenbergMarquardt1,+ levenbergMarquardt1WithWeights,+ levenbergMarquardt1WithYerrors,+ levenbergMarquardt1WithXYerrors,+ -- ** Two parameters+ levenbergMarquardt2,+ levenbergMarquardt2WithWeights,+ levenbergMarquardt2WithYerrors,+ levenbergMarquardt2WithXYerrors,+ -- ** Three parameters+ levenbergMarquardt3,+ levenbergMarquardt3WithWeights,+ levenbergMarquardt3WithYerrors,+ levenbergMarquardt3WithXYerrors,+ -- * Auxiliary types+ Fit (..),+ V2 (..),+ V3 (..),+) where++import Control.DeepSeq (NFData (..))++import qualified Data.Foldable as F+import qualified Data.List.NonEmpty as NE++import Math.Regression.Simple.LinAlg+import Numeric.KBN++-- $setup+-- >>> :set -XDeriveFunctor -XDeriveFoldable -XDeriveTraversable+--+-- >>> import Numeric (showFFloat)+-- >>> import Data.List.NonEmpty (NonEmpty (..))+-- >>> import qualified Data.List.NonEmpty as NE+--+-- Don't show too much decimal digits+--+-- >>> let showDouble x = showFFloat (Just (min 5 (5 - ceiling (logBase 10 x)))) (x :: Double)+-- >>> showDouble 123.456 ""+-- "123.46"+--+-- >>> showDouble 1234567 ""+-- "1234567"+--+-- >>> showDouble 123.4567890123456789 ""+-- "123.46"+--+-- >>> showDouble 0.0000000000000012345 ""+-- "0.00000"+--+-- >>> newtype PP a = PP a+-- >>> class Show' a where showsPrec' :: Int -> a -> ShowS+-- >>> instance Show' a => Show (PP a) where showsPrec d (PP x) = showsPrec' d x+-- >>> instance Show' Double where showsPrec' d x = if x < 0 then showParen (d > 6) (showChar '-' . showDouble (negate x)) else showDouble x+-- >>> instance Show' Int where showsPrec' = showsPrec+-- >>> instance (Show' a, Show' b) => Show' (a, b) where showsPrec' d (x, y) = showParen True $ showsPrec' 0 x . showString ", " . showsPrec' 0 y+-- >>> instance Show' v => Show' (Fit v) where showsPrec' d (Fit p e ndf wssr) = showParen (d > 10) $ showString "Fit " . showsPrec' 11 p . showChar ' ' . showsPrec' 11 e . showChar ' ' . showsPrec' 11 ndf . showChar ' ' . showsPrec' 11 wssr+-- >>> instance Show' V2 where showsPrec' d (V2 x y) = showParen (d > 10) $ showString "V2 " . showsPrec' 11 x . showChar ' ' . showsPrec' 11 y+-- >>> instance Show' V3 where showsPrec' d (V3 x y z) = showParen (d > 10) $ showString "V3 " . showsPrec' 11 x . showChar ' ' . showsPrec' 11 y . showChar ' ' . showsPrec' 11 z+-- >>> instance Show' a => Show' [a] where showsPrec' _ [] = id; showsPrec' _ [x] = showsPrec' 0 x; showsPrec' _ (x:xs) = showsPrec' 0 x . showChar '\n' . showsPrec' 0 xs+-- >>> instance Show' a => Show' (NonEmpty a) where showsPrec' _ (x :| []) = showsPrec' 0 x; showsPrec' _ (x:|xs) = showsPrec' 0 x . showChar '\n' . showsPrec' 0 xs+-- >>>+--+-- Inputs:+-- >>> let input1 = [(0, 1), (1, 3), (2, 5)]+-- >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]+-- >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]+--+-- >>> let sq z = z * z+--++-------------------------------------------------------------------------------+-- Linear+-------------------------------------------------------------------------------++-- | Linear regression.+--+-- >>> let input1 = [(0, 1), (1, 3), (2, 5)]+-- >>> PP $ linear id input1+-- V2 2.0000 1.00000+--+-- >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]+-- >>> PP $ linear id input2+-- V2 2.0063 0.88685+--+linear :: F.Foldable f => (a -> (Double, Double)) -> f a -> V2+linear f = fitParams . linearFit f++-- | Like 'linear' but returns complete 'Fit'.+--+-- To get confidence intervals you should multiply the errors+-- by @quantile (studentT (n - 2)) ci'@ from @statistics@ package+-- or similar.+-- For big @n@ using value 1 gives 68% interval and using value 2 gives 95% confidence interval.+-- See https://en.wikipedia.org/wiki/Student%27s_t-distribution#Table_of_selected_values+-- (@quantile@ calculates one-sided values, you need two-sided, thus adjust @ci@ value).+--+-- The first input is perfect fit:+--+-- >>> let fit = linearFit id input1+-- >>> PP fit+-- Fit (V2 2.0000 1.00000) (V2 0.00000 0.00000) 1 0.00000+--+-- The second input is quite good:+--+-- >>> PP $ linearFit id input2+-- Fit (V2 2.0063 0.88685) (V2 0.09550 0.23826) 3 0.25962+--+-- But the third input isn't so much,+-- standard error of a slope parameter is 20%.+--+-- >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]+-- >>> PP $ linearFit id input3+-- Fit (V2 3.0000 1.00000) (V2 0.63246 1.1832) 2 4.0000+--+linearFit :: F.Foldable f => (a -> (Double, Double)) -> f a -> Fit V2+linearFit f = linearFit' . linRegAcc f++-- | Weighted linear regression.+--+-- >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]+-- >>> PP $ linearFit id input2+-- Fit (V2 2.0063 0.88685) (V2 0.09550 0.23826) 3 0.25962+--+-- >>> let input2w = [(0.1, 1.2, 1), (1.3, 3.1, 1), (1.9, 4.9, 1), (3.0, 7.1, 1/4), (4.1, 9.0, 1/4)]+-- >>> PP $ linearWithWeights id input2w+-- Fit (V2 2.0060 0.86993) (V2 0.12926 0.23696) 3 0.22074+--+linearWithWeights :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> Fit V2+linearWithWeights f = linearFit' . linRegAccW f++-- | Linear regression with y-errors.+--+-- >>> let input2y = [(0.1, 1.2, 0.12), (1.3, 3.1, 0.31), (1.9, 4.9, 0.49), (3.0, 7.1, 0.71), (4.1, 9.0, 1.9)]+-- >>> let fit = linearWithYerrors id input2y+-- >>> PP fit+-- Fit (V2 1.9104 0.98302) (V2 0.13006 0.10462) 3 2.0930+--+-- When we know actual y-errors, we can calculate the Q-value using @statistics@ package:+--+-- >>> import qualified Statistics.Distribution as S+-- >>> import qualified Statistics.Distribution.ChiSquared as S+-- >>> S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)+-- 0.446669639443138+--+-- or using @math-functions@+--+-- >>> import Numeric.SpecFunctions (incompleteGamma)+-- >>> incompleteGamma (fromIntegral (fitNDF fit) / 2) (fitWSSR fit / 2)+-- 0.446669639443138+--+-- It is not uncommon to deem acceptable on equal terms any models with, say, Q > 0.001.+-- If Q is too large, too near to 1 is most likely caused by overestimating+-- the y-errors.+--+--+linearWithYerrors :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> Fit V2+linearWithYerrors f = linearWithWeights f' where+ f' a = case f a of+ (x, y, dy) -> (x, y, recip (dy * dy))++-- | Iterative linear regression with x and y errors.+--+-- /Orear, J. (1982). Least squares when both variables have uncertainties. American Journal of Physics, 50(10), 912–916. doi:10.1119\/1.12972/+--+-- >>> let input2xy = [(0.1, 1.2, 0.01, 0.12), (1.3, 3.1, 0.13, 0.31), (1.9, 4.9, 0.19, 0.49), (3.0, 7.1, 0.3, 0.71), (4.1, 9.0, 0.41, 1.9)]+-- >>> let fit :| fits = linearWithXYerrors id input2xy+--+-- First fit is done using 'linearWithYerrors':+--+-- >>> PP fit+-- Fit (V2 1.9104 0.98302) (V2 0.13006 0.10462) 3 2.0930+--+-- After that the effective variance is used to refine the fit,+-- just a few iterations is often enough:+--+-- >>> PP $ take 3 fits+-- Fit (V2 1.9092 0.99251) (V2 0.12417 0.08412) 3 1.2992+-- Fit (V2 1.9092 0.99250) (V2 0.12418 0.08414) 3 1.2998+-- Fit (V2 1.9092 0.99250) (V2 0.12418 0.08414) 3 1.2998+--+linearWithXYerrors+ :: F.Foldable f+ => (a -> (Double, Double, Double, Double)) -- ^ \(x_i, y_i, \delta x_i, \delta y_i\)+ -> f a -- ^ data+ -> NE.NonEmpty (Fit V2)+linearWithXYerrors f xs = iterate1 go fit0 where+ fit0 = linearWithYerrors (\a -> case f a of (x,y,_,dy) -> (x,y,dy)) xs+ go fit = linearWithWeights (\a -> case f a of (x,y,dx,dy) -> (x,y,recip $ sq (param1 * dx) + sq dy)) xs where+ V2 param1 _ = fitParams fit++-- >>> import qualified Numeric.AD.Mode.Reverse.Double as AD+-- >>> data H3 a = H3 a a a deriving (Functor, Foldable, Traversable)+-- >>> let linearF (H3 a b x) = a * x + b+-- >>> let lin' (V2 a b) (x, y, dx, dy) = case AD.grad' linearF (H3 a b x) of (f, H3 da db f') -> (y, f, V2 da db, recip $ sq (f' * dx) + sq dy)+--+-- >>> PP $ NE.last $ levenbergMarquardt2WithWeights lin' (V2 1 1) input2xy+-- Fit (V2 1.9092 0.99250) (V2 0.12418 0.08414) 3 1.2998++-- | Calculate linear fit from 'LinRegAcc'.+linearFit' :: LinRegAcc -> Fit V2+linearFit' LinRegAcc {..} = Fit params errors ndf wssr where+ matrix@(SM22 a11 _ a22) = inv (SM22 x2 x w)+ params@(V2 a b) = mult matrix (V2 xy y)++ errors = V2 sa sb++ -- ensure that error is always non-negative.+ -- Due rounding errors, in perfect fit situations it can be slightly negative.+ wssr = max 0 (y2 - a * xy - b * y)+ ndf = lra_n - 2+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')++ w = getKBN lra_w+ x = getKBN lra_x+ x2 = getKBN lra_x2+ y = getKBN lra_y+ xy = getKBN lra_xy+ y2 = getKBN lra_y2++ -- is it useful?+ -- r2 = (n * xy - x*y) / (sqrt (n * x2 - x*x) * sqrt (n * y2 - y*y))++linRegAcc :: F.Foldable f => (a -> (Double, Double)) -> f a -> LinRegAcc+linRegAcc f = F.foldl' (\acc a -> case f a of (x,y) -> addLinReg acc x y) zeroLinRegAcc++linRegAccW :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> LinRegAcc+linRegAccW f = F.foldl' (\acc a -> case f a of (x,y,w) -> addLinRegW acc x y w) zeroLinRegAcc++-------------------------------------------------------------------------------+-- Quadractic+-------------------------------------------------------------------------------++-- | Quadratic regression.+--+-- >>> let input1 = [(0, 1), (1, 3), (2, 5)]+-- >>> quadratic id input1+-- V3 0.0 2.0 1.0+--+-- >>> let input2 = [(0.1, 1.2), (1.3, 3.1), (1.9, 4.9), (3.0, 7.1), (4.1, 9.0)]+-- >>> PP $ quadratic id input2+-- V3 (-0.00589) 2.0313 0.87155+--+-- >>> let input3 = [(0, 2), (1, 3), (2, 6), (3, 11)]+-- >>> PP $ quadratic id input3+-- V3 1.00000 0.00000 2.0000+--+quadratic :: F.Foldable f => (a -> (Double, Double)) -> f a -> V3+quadratic f = fitParams . quadraticFit f++-- | Like 'quadratic' but returns complete 'Fit'.+--+-- >>> PP $ quadraticFit id input2+-- Fit (V3 (-0.00589) 2.0313 0.87155) (V3 0.09281 0.41070 0.37841) 2 0.25910+--+-- >>> PP $ quadraticFit id input3+-- Fit (V3 1.00000 0.00000 2.0000) (V3 0.00000 0.00000 0.00000) 1 0.00000+--+quadraticFit :: F.Foldable f => (a -> (Double, Double)) -> f a -> Fit V3+quadraticFit f = quadraticFit' . quadRegAcc f++-- | Weighted quadratic regression.+--+-- >>> let input2w = [(0.1, 1.2, 1), (1.3, 3.1, 1), (1.9, 4.9, 1), (3.0, 7.1, 1/4), (4.1, 9.0, 1/4)]+-- >>> PP $ quadraticWithWeights id input2w+-- Fit (V3 0.02524 1.9144 0.91792) (V3 0.10775 0.42106 0.35207) 2 0.21484+--+quadraticWithWeights :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> Fit V3+quadraticWithWeights f = quadraticFit' . quadRegAccW f++-- | Quadratic regression with y-errors.+--+-- >>> let input2y = [(0.1, 1.2, 0.12), (1.3, 3.1, 0.31), (1.9, 4.9, 0.49), (3.0, 7.1, 0.71), (4.1, 9.0, 0.9)]+-- >>> PP $ quadraticWithYerrors id input2y+-- Fit (V3 0.08776 1.6667 1.0228) (V3 0.10131 0.31829 0.11917) 2 1.5398+--+quadraticWithYerrors :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> Fit V3+quadraticWithYerrors f = quadraticWithWeights f' where+ f' a = case f a of+ (x, y, dy) -> (x, y, recip (dy * dy))++-- | Iterative quadratic regression with x and y errors.+--+-- /Orear, J. (1982). Least squares when both variables have uncertainties. American Journal of Physics, 50(10), 912–916. doi:10.1119\/1.12972/+--+quadraticWithXYerrors+ :: F.Foldable f+ => (a -> (Double, Double, Double, Double)) -- ^ \(x_i, y_i, \delta x_i, \delta y_i\)+ -> f a -- ^ data+ -> NE.NonEmpty (Fit V3)+quadraticWithXYerrors f xs = iterate1 go fit0 where+ fit0 = quadraticWithYerrors (\a -> case f a of (x,y,_,dy) -> (x,y,dy)) xs+ go fit = quadraticWithWeights (\a -> case f a of (x,y,dx,dy) -> (x,y,recip $ sq ((2 * p1 * x + p2) * dx) + sq dy)) xs where+ V3 p1 p2 _ = fitParams fit++-- | Calculate quadratic fit from 'QuadRegAcc'.+quadraticFit' :: QuadRegAcc -> Fit V3+quadraticFit' QuadRegAcc {..} = Fit params errors ndf wssr where+ matrix@(SM33 a11+ _ a22+ _ _ a33) = inv (SM33 x4+ x3 x2+ x2 x w)++ params@(V3 a b c) = mult matrix (V3 x2y xy y)++ errors = V3 sa sb sc++ wssr = max 0 (y2 - a * x2y - b * xy - c * y)+ ndf = qra_n - 3+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')+ sc = sqrt (a33 * wssr / ndf')++ w = getKBN qra_w+ x = getKBN qra_x+ x2 = getKBN qra_x2+ x3 = getKBN qra_x3+ x4 = getKBN qra_x4+ y = getKBN qra_y+ xy = getKBN qra_xy+ x2y = getKBN qra_x2y+ y2 = getKBN qra_y2++ -- is it useful?+ -- r2 = (n * xy - x*y) / (sqrt (n * x2 - x*x) * sqrt (n * y2 - y*y))++quadRegAcc :: F.Foldable f => (a -> (Double, Double)) -> f a -> QuadRegAcc+quadRegAcc f = F.foldl' (\acc a -> case f a of (x,y) -> addQuadReg acc x y) zeroQuadRegAcc++quadRegAccW :: F.Foldable f => (a -> (Double, Double, Double)) -> f a -> QuadRegAcc+quadRegAccW f = F.foldl' (\acc a -> case f a of (x,y,w) -> addQuadRegW acc x y w) zeroQuadRegAcc++-------------------------------------------------------------------------------+-- Levenberg–Marquardt 1+-------------------------------------------------------------------------------++-- | Levenberg–Marquardt for functions with one parameter.+--+-- See 'levenbergMarquardt2' for examples, this is very similar.+--+-- For example we can fit \(f = x \mapsto \beta x + 1\), its derivative is \(\partial_\beta f = x \mapsto x\).+--+-- >>> let scale a (x, y) = (y, a * x + 1, x)+-- >>> PP $ NE.last $ levenbergMarquardt1 scale 1 input2+-- Fit 1.9685 0.04735 4 0.27914+--+-- Not bad, but worse then linear fit which fits the intercept point too.+--+levenbergMarquardt1+ :: F.Foldable f+ => (Double -> a -> (Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \partial_\beta f(\beta, x)\)+ -> Double -- ^ initial parameter, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit Double) -- ^ non-empty list of iteration results+levenbergMarquardt1 f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ lambda0 = c11+ where+ c11 = getKBN $ lm1_c11 acc0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d) -> addLM1Acc acc y g d) zeroLM1Acc xs++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (lambda1 c11)+ delta = mult matrix z1++ beta' = add beta delta+ acc' = calcAcc beta'++ a11 = inv c11+ errors = sa++ wssr = max 0 $ getKBN $ lm1_wssr acc+ wssr' = getKBN $ lm1_wssr acc'++ ndf = lm1_n acc - 1+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')++ c11 = getKBN $ lm1_c11 acc+ z1 = getKBN $ lm1_z1 acc++-- | 'levenbergMarquardt1' with weights.+levenbergMarquardt1WithWeights+ :: F.Foldable f+ => (Double -> a -> (Double, Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \partial_\beta f(\beta, x), w_i\)+ -> Double -- ^ initial parameter, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit Double) -- ^ non-empty list of iteration results+levenbergMarquardt1WithWeights f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ lambda0 = c11+ where+ c11 = getKBN $ lm1_c11 acc0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d, w) -> addLM1AccW acc y g d w) zeroLM1Acc xs++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (lambda1 c11)+ delta = mult matrix z1++ beta' = add beta delta+ acc' = calcAcc beta'++ a11 = inv c11+ errors = sa++ wssr = max 0 $ getKBN $ lm1_wssr acc+ wssr' = getKBN $ lm1_wssr acc'++ ndf = lm1_n acc - 1+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')++ c11 = getKBN $ lm1_c11 acc+ z1 = getKBN $ lm1_z1 acc++-- | 'levenbergMarquardt1' with Y-errors.+levenbergMarquardt1WithYerrors+ :: F.Foldable f+ => (Double -> a -> (Double, Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \partial_\beta f(\beta, x), \delta y_i\)+ -> Double -- ^ initial parameter, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit Double) -- ^ non-empty list of iteration results+levenbergMarquardt1WithYerrors f = levenbergMarquardt1WithWeights f' where+ f' beta x = case f beta x of (y, fbetax, grad, dy) -> (y, fbetax, grad, recip $ sq dy)++-- | 'levenbergMarquardt1' with XY-errors.+levenbergMarquardt1WithXYerrors+ :: F.Foldable f+ => (Double -> a -> (Double, Double, Double, Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \partial_\beta f(\beta, x), \partial_x f(\beta, x), \delta x_i, \delta y_i\)+ -> Double -- ^ initial parameter, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit Double) -- ^ non-empty list of iteration results+levenbergMarquardt1WithXYerrors g = levenbergMarquardt1WithWeights g' where+ g' beta x = case g beta x of (y, fbetax, grad, f', dx, dy) -> (y, fbetax, grad, recip $ sq (f' * dx) + sq dy)++data LM1Acc = LM1Acc+ { lm1_n :: !Int+ , lm1_c11 :: !KBN+ , lm1_z1 :: !KBN+ , lm1_wssr :: !KBN+ }+ deriving Show++zeroLM1Acc :: LM1Acc+zeroLM1Acc = LM1Acc 0 zeroKBN zeroKBN zeroKBN++addLM1Acc :: LM1Acc -> Double -> Double -> Double -> LM1Acc+addLM1Acc LM1Acc {..} y f d1 = LM1Acc+ { lm1_n = lm1_n + 1+ , lm1_c11 = addKBN lm1_c11 (d1 * d1)+ , lm1_z1 = addKBN lm1_z1 (d1 * res)+ , lm1_wssr = addKBN lm1_wssr (res * res)+ }+ where+ res = y - f++addLM1AccW :: LM1Acc -> Double -> Double -> Double -> Double -> LM1Acc+addLM1AccW LM1Acc {..} y f d1 w = LM1Acc+ { lm1_n = lm1_n + 1+ , lm1_c11 = addKBN lm1_c11 (w * d1 * d1)+ , lm1_z1 = addKBN lm1_z1 (w * d1 * res)+ , lm1_wssr = addKBN lm1_wssr (w * res * res)+ }+ where+ res = y - f++-------------------------------------------------------------------------------+-- Levenberg–Marquardt 2+-------------------------------------------------------------------------------++-- | Levenberg–Marquardt for functions with two parameters.+--+-- You can use this sledgehammer to do a a linear fit:+--+-- >>> let lin (V2 a b) (x, y) = (y, a * x + b, V2 x 1)+--+-- We can then use 'levenbergMarquardt2' to find a fit:+--+-- >>> PP $ levenbergMarquardt2 lin (V2 1 1) input2+-- Fit (V2 1.00000 1.00000) (V2 1.0175 2.5385) 3 29.470+-- Fit (V2 1.0181 1.0368) (V2 0.98615 2.4602) 3 27.681+-- Fit (V2 1.1557 1.2988) (V2 0.75758 1.8900) 3 16.336+-- Fit (V2 1.5463 1.6577) (V2 0.29278 0.73043) 3 2.4400+-- Fit (V2 1.9129 1.1096) (V2 0.11033 0.27524) 3 0.34645+-- Fit (V2 2.0036 0.89372) (V2 0.09552 0.23830) 3 0.25970+-- Fit (V2 2.0063 0.88687) (V2 0.09550 0.23826) 3 0.25962+-- Fit (V2 2.0063 0.88685) (V2 0.09550 0.23826) 3 0.25962+--+-- This is the same result what 'linearFit' returns:+--+-- >>> PP $ linearFit id input2+-- Fit (V2 2.0063 0.88685) (V2 0.09550 0.23826) 3 0.25962+--+-- == Using AD+--+-- You can use @ad@ to calculate derivatives for you.+--+-- >>> import qualified Numeric.AD.Mode.Reverse.Double as AD+--+-- We need a ('Traversable') homogenic triple to represent the two parameters and @x@:+--+-- >>> data H3 a = H3 a a a deriving (Functor, Foldable, Traversable)+--+-- Then we define a function @ad@ can operate with:+--+-- >>> let linearF (H3 a b x) = a * x + b+--+-- which we can use to fit the curve in generic way:+--+-- >>> let lin' (V2 a b) (x, y) = case AD.grad' linearF (H3 a b x) of (f, H3 da db _f') -> (y, f, V2 da db)+-- >>> PP $ levenbergMarquardt2 lin' (V2 1 1) input2+-- Fit (V2 1.00000 1.00000) (V2 1.0175 2.5385) 3 29.470+-- Fit (V2 1.0181 1.0368) (V2 0.98615 2.4602) 3 27.681+-- Fit (V2 1.1557 1.2988) (V2 0.75758 1.8900) 3 16.336+-- Fit (V2 1.5463 1.6577) (V2 0.29278 0.73043) 3 2.4400+-- Fit (V2 1.9129 1.1096) (V2 0.11033 0.27524) 3 0.34645+-- Fit (V2 2.0036 0.89372) (V2 0.09552 0.23830) 3 0.25970+-- Fit (V2 2.0063 0.88687) (V2 0.09550 0.23826) 3 0.25962+-- Fit (V2 2.0063 0.88685) (V2 0.09550 0.23826) 3 0.25962+--+-- == Non-polynomial example+--+-- We can fit other curves too, for example an example from Wikipedia+-- https://en.wikipedia.org/wiki/Gauss%E2%80%93Newton_algorithm#Example+--+-- >>> let rateF (H3 vmax km s) = (vmax * s) / (km + s)+-- >>> let rateF' (V2 vmax km) (x, y) = case AD.grad' rateF (H3 vmax km x) of (f, H3 vmax' km' _) -> (y, f, V2 vmax' km')+-- >>> let input = zip [0.038,0.194,0.425,0.626,1.253,2.500,3.740] [0.050,0.127,0.094,0.2122,0.2729,0.2665,0.3317]+-- >>> PP $ levenbergMarquardt2 rateF' (V2 0.9 0.2) input+-- Fit (V2 0.90000 0.20000) (V2 0.43304 0.43936) 5 1.4455+-- Fit (V2 0.83306 0.25278) (V2 0.39164 0.49729) 5 1.0055+-- Fit (V2 0.59437 0.43508) (V2 0.21158 0.53403) 5 0.18832+-- Fit (V2 0.39687 0.56324) (V2 0.05723 0.25666) 5 0.01062+-- Fit (V2 0.36289 0.56104) (V2 0.04908 0.24007) 5 0.00784+-- Fit (V2 0.36190 0.55662) (V2 0.04887 0.23843) 5 0.00784+-- Fit (V2 0.36184 0.55629) (V2 0.04885 0.23830) 5 0.00784+-- Fit (V2 0.36184 0.55627) (V2 0.04885 0.23829) 5 0.00784+--+-- We get the same result as in the article: 0.362 and 0.556+--+-- The algorithm terminates when a scaling parameter \(\lambda\) becomes larger than 1e20 or smaller than 1e-20, or relative WSSR change is smaller than 1e-10, or sum-of-squared-residuals candidate becomes @NaN@ (i.e. when it would start to produce garbage).+-- You may want to terminate sooner, Numerical Recipes suggest to stop when WSSR decreases by a neglible amount absolutely or fractionally.+--+levenbergMarquardt2+ :: F.Foldable f+ => (V2 -> a -> (Double, Double, V2)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x)\)+ -> V2 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V2) -- ^ non-empty list of iteration results+levenbergMarquardt2 f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d) -> addLM2Acc acc y g d) zeroLM2Acc xs++ lambda0 = max l1 l2+ where+ V2 l1 l2 = eigenSM22 c11 c12 c22+ c11 = getKBN $ lm2_c11 acc0+ c12 = getKBN $ lm2_c11 acc0+ c22 = getKBN $ lm2_c22 acc0++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (SM22 (lambda1 c11) c12 (lambda1 c22))+ delta = mult matrix (V2 z1 z2)++ beta' = add beta delta+ acc' = calcAcc beta'++ SM22 a11 _ a22 = inv (SM22 c11 c12 c22)+ errors = V2 sa sb++ wssr = max 0 $ getKBN $ lm2_wssr acc+ wssr' = getKBN $ lm2_wssr acc'++ ndf = lm2_n acc - 2+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')++ c11 = getKBN $ lm2_c11 acc+ c12 = getKBN $ lm2_c12 acc+ c22 = getKBN $ lm2_c22 acc+ z1 = getKBN $ lm2_z1 acc+ z2 = getKBN $ lm2_z2 acc++-- | 'levenbergMarquardt2' with weights.+--+-- Because 'levenbergMarquardt2' is an iterative algorithm,+-- not only we can use it to fit curves with known y-errors ('levenbergMarquardt2WithYerrors'),+-- but also with both x and y-errors ('levenbergMarquardt2WithXYerrors').+--+levenbergMarquardt2WithWeights+ :: F.Foldable f+ => (V2 -> a -> (Double, Double, V2, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), w_i\)+ -> V2 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V2) -- ^ non-empty list of iteration results+levenbergMarquardt2WithWeights f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ lambda0 = max l1 l2+ where+ V2 l1 l2 = eigenSM22 c11 c12 c22+ c11 = getKBN $ lm2_c11 acc0+ c12 = getKBN $ lm2_c11 acc0+ c22 = getKBN $ lm2_c22 acc0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d, w) -> addLM2AccW acc y g d w) zeroLM2Acc xs++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (SM22 (lambda1 c11) c12 (lambda1 c22))+ delta = mult matrix (V2 z1 z2)++ beta' = add beta delta+ acc' = calcAcc beta'++ SM22 a11 _ a22 = inv (SM22 c11 c12 c22)+ errors = V2 sa sb++ wssr = max 0 $ getKBN $ lm2_wssr acc+ wssr' = getKBN $ lm2_wssr acc'++ ndf = lm2_n acc - 2+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')++ c11 = getKBN $ lm2_c11 acc+ c12 = getKBN $ lm2_c12 acc+ c22 = getKBN $ lm2_c22 acc+ z1 = getKBN $ lm2_z1 acc+ z2 = getKBN $ lm2_z2 acc++-- | 'levenbergMarquardt2' with Y-errors.+levenbergMarquardt2WithYerrors+ :: F.Foldable f+ => (V2 -> a -> (Double, Double, V2, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), \delta y_i\)+ -> V2 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V2) -- ^ non-empty list of iteration results+levenbergMarquardt2WithYerrors f = levenbergMarquardt2WithWeights f' where+ f' beta x = case f beta x of (y, fbetax, grad, dy) -> (y, fbetax, grad, recip $ sq dy)++-- | 'levenbergMarquardt2' with XY-errors.+levenbergMarquardt2WithXYerrors+ :: F.Foldable f+ => (V2 -> a -> (Double, Double, V2, Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), \partial_x f(\beta, x), \delta x_i, \delta y_i\)+ -> V2 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V2) -- ^ non-empty list of iteration results+levenbergMarquardt2WithXYerrors g = levenbergMarquardt2WithWeights g' where+ g' beta x = case g beta x of (y, fbetax, grad, f', dx, dy) -> (y, fbetax, grad, recip $ sq (f' * dx) + sq dy)++data LM2Acc = LM2Acc+ { lm2_n :: !Int+ , lm2_c11 :: !KBN+ , lm2_c12 :: !KBN+ , lm2_c22 :: !KBN+ , lm2_z1 :: !KBN+ , lm2_z2 :: !KBN+ , lm2_wssr :: !KBN+ }+ deriving Show++zeroLM2Acc :: LM2Acc+zeroLM2Acc = LM2Acc 0 zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN++addLM2Acc :: LM2Acc -> Double -> Double -> V2 -> LM2Acc+addLM2Acc LM2Acc {..} y f (V2 d1 d2) = LM2Acc+ { lm2_n = lm2_n + 1+ , lm2_c11 = addKBN lm2_c11 (d1 * d1)+ , lm2_c12 = addKBN lm2_c12 (d1 * d2)+ , lm2_c22 = addKBN lm2_c22 (d2 * d2)+ , lm2_z1 = addKBN lm2_z1 (d1 * res)+ , lm2_z2 = addKBN lm2_z2 (d2 * res)+ , lm2_wssr = addKBN lm2_wssr (res * res)+ }+ where+ res = y - f++addLM2AccW :: LM2Acc -> Double -> Double -> V2 -> Double -> LM2Acc+addLM2AccW LM2Acc {..} y f (V2 d1 d2) w = LM2Acc+ { lm2_n = lm2_n + 1+ , lm2_c11 = addKBN lm2_c11 (w * d1 * d1)+ , lm2_c12 = addKBN lm2_c12 (w * d1 * d2)+ , lm2_c22 = addKBN lm2_c22 (w * d2 * d2)+ , lm2_z1 = addKBN lm2_z1 (w * d1 * res)+ , lm2_z2 = addKBN lm2_z2 (w * d2 * res)+ , lm2_wssr = addKBN lm2_wssr (w * res * res)+ }+ where+ res = y - f++-------------------------------------------------------------------------------+-- Levenberg–Marquardt 3+-------------------------------------------------------------------------------++-- | Levenberg–Marquardt for functions with three parameters.+--+-- See 'levenbergMarquardt2' for examples, this is very similar.+--+-- >>> let quad (V3 a b c) (x, y) = (y, a * x * x + b * x + c, V3 (x * x) x 1)+-- >>> PP $ NE.last $ levenbergMarquardt3 quad (V3 2 2 2) input3+-- Fit (V3 1.00000 0.00000 2.0000) (V3 0.00000 0.00000 0.00000) 1 0.00000+--+-- Same as quadratic fit, just less direct:+--+-- >>> PP $ quadraticFit id input3+-- Fit (V3 1.00000 0.00000 2.0000) (V3 0.00000 0.00000 0.00000) 1 0.00000+--+levenbergMarquardt3+ :: F.Foldable f+ => (V3 -> a -> (Double, Double, V3)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x)\)+ -> V3 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V3) -- ^ non-empty list of iteration results+levenbergMarquardt3 f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d) -> addLM3Acc acc y g d) zeroLM3Acc xs++ -- frobenius norm is larger than largest eigen value.+ -- calculating the eigen values for 3x3 (symmetric) matrix is becoming complicated.+ lambda0 = sqrt $ sumKBN [ sq c11+ , 2 * sq c12, sq c22+ , 2 * sq c13, 2 * sq c23, sq c33]+ where+ c11 = getKBN $ lm3_c11 acc0+ c12 = getKBN $ lm3_c12 acc0+ c13 = getKBN $ lm3_c13 acc0+ c22 = getKBN $ lm3_c22 acc0+ c23 = getKBN $ lm3_c23 acc0+ c33 = getKBN $ lm3_c33 acc0++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (SM33 (lambda1 c11)+ c12 (lambda1 c22)+ c13 c23 (lambda1 c33))+ delta = mult matrix (V3 z1 z2 z3)++ beta' = add beta delta+ acc' = calcAcc beta'++ SM33 a11+ _ a22+ _ _ a33 = inv (SM33 c11+ c12 c22+ c13 c23 c33)+ errors = V3 sa sb sc++ wssr = max 0 $ getKBN $ lm3_wssr acc+ wssr' = getKBN $ lm3_wssr acc'++ ndf = lm3_n acc - 3+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')+ sc = sqrt (a33 * wssr / ndf')++ c11 = getKBN $ lm3_c11 acc+ c12 = getKBN $ lm3_c12 acc+ c13 = getKBN $ lm3_c13 acc+ c22 = getKBN $ lm3_c22 acc+ c23 = getKBN $ lm3_c23 acc+ c33 = getKBN $ lm3_c33 acc+ z1 = getKBN $ lm3_z1 acc+ z2 = getKBN $ lm3_z2 acc+ z3 = getKBN $ lm3_z3 acc++-- | 'levenbergMarquardt3' with weights.+levenbergMarquardt3WithWeights+ :: F.Foldable f+ => (V3 -> a -> (Double, Double, V3, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), w_i\)+ -> V3 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V3) -- ^ non-empty list of iteration results+levenbergMarquardt3WithWeights f b0 xs = loop lambda0 b0 acc0 where+ acc0 = calcAcc b0++ lambda0 = sqrt $ sumKBN [ sq c11+ , 2 * sq c12, sq c22+ , 2 * sq c13, 2 * sq c23, sq c33]+ where+ c11 = getKBN $ lm3_c11 acc0+ c12 = getKBN $ lm3_c12 acc0+ c13 = getKBN $ lm3_c13 acc0+ c22 = getKBN $ lm3_c22 acc0+ c23 = getKBN $ lm3_c23 acc0+ c33 = getKBN $ lm3_c33 acc0++ calcAcc beta = F.foldl' (\acc p -> case f beta p of (y, g, d, w) -> addLM3AccW acc y g d w) zeroLM3Acc xs++ loop lambda beta acc+ | lmStop lambda wssr wssr'+ = Fit beta errors ndf wssr NE.:| []++ | wssr' >= wssr+ = loop (lambda * 10) beta acc++ | otherwise+ = Fit beta errors ndf wssr `NE.cons` loop (lambda / 10) beta' acc'++ where+ lambda1 z = (1 + lambda) * z+ matrix = inv (SM33 (lambda1 c11)+ c12 (lambda1 c22)+ c13 c23 (lambda1 c33))+ delta = mult matrix (V3 z1 z2 z3)++ beta' = add beta delta+ acc' = calcAcc beta'++ SM33 a11 _ _ a22 _ a33 = inv (SM33 c11 c12 c13 c22 c23 c33)+ errors = V3 sa sb sc++ wssr = max 0 $ getKBN $ lm3_wssr acc+ wssr' = getKBN $ lm3_wssr acc'++ ndf = lm3_n acc - 3+ ndf' = fromIntegral ndf :: Double++ sa = sqrt (a11 * wssr / ndf')+ sb = sqrt (a22 * wssr / ndf')+ sc = sqrt (a33 * wssr / ndf')++ c11 = getKBN $ lm3_c11 acc+ c12 = getKBN $ lm3_c12 acc+ c13 = getKBN $ lm3_c13 acc+ c22 = getKBN $ lm3_c22 acc+ c23 = getKBN $ lm3_c23 acc+ c33 = getKBN $ lm3_c33 acc+ z1 = getKBN $ lm3_z1 acc+ z2 = getKBN $ lm3_z2 acc+ z3 = getKBN $ lm3_z3 acc++-- | 'levenbergMarquardt3' with Y-errors.+levenbergMarquardt3WithYerrors+ :: F.Foldable f+ => (V3 -> a -> (Double, Double, V3, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), \delta y_i\)+ -> V3 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V3) -- ^ non-empty list of iteration results+levenbergMarquardt3WithYerrors f = levenbergMarquardt3WithWeights f' where+ f' beta x = case f beta x of (y, fbetax, grad, dy) -> (y, fbetax, grad, recip $ sq dy)++-- | 'levenbergMarquardt3' with XY-errors.+levenbergMarquardt3WithXYerrors+ :: F.Foldable f+ => (V3 -> a -> (Double, Double, V3, Double, Double, Double)) -- ^ \(\beta, d_i \mapsto y_i, f(\beta, x_i), \nabla_\beta f(\beta, x), \partial_x f(\beta, x), \delta x_i, \delta y_i\)+ -> V3 -- ^ initial parameters, \(\beta_0\)+ -> f a -- ^ data, \(d\)+ -> NE.NonEmpty (Fit V3) -- ^ non-empty list of iteration results+levenbergMarquardt3WithXYerrors g = levenbergMarquardt3WithWeights g' where+ g' beta x = case g beta x of (y, fbetax, grad, f', dx, dy) -> (y, fbetax, grad, recip $ sq (f' * dx) + sq dy)++data LM3Acc = LM3Acc+ { lm3_n :: !Int+ , lm3_c11 :: !KBN+ , lm3_c12 :: !KBN+ , lm3_c13 :: !KBN+ , lm3_c22 :: !KBN+ , lm3_c23 :: !KBN+ , lm3_c33 :: !KBN+ , lm3_z1 :: !KBN+ , lm3_z2 :: !KBN+ , lm3_z3 :: !KBN+ , lm3_wssr :: !KBN+ }+ deriving Show++zeroLM3Acc :: LM3Acc+zeroLM3Acc = LM3Acc 0 zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN++addLM3Acc :: LM3Acc -> Double -> Double -> V3 -> LM3Acc+addLM3Acc LM3Acc {..} y f (V3 d1 d2 d3) = LM3Acc+ { lm3_n = lm3_n + 1+ , lm3_c11 = addKBN lm3_c11 (d1 * d1)+ , lm3_c12 = addKBN lm3_c12 (d1 * d2)+ , lm3_c13 = addKBN lm3_c12 (d1 * d3)+ , lm3_c22 = addKBN lm3_c22 (d2 * d2)+ , lm3_c23 = addKBN lm3_c22 (d2 * d3)+ , lm3_c33 = addKBN lm3_c22 (d3 * d3)+ , lm3_z1 = addKBN lm3_z1 (d1 * res)+ , lm3_z2 = addKBN lm3_z2 (d2 * res)+ , lm3_z3 = addKBN lm3_z3 (d3 * res)+ , lm3_wssr = addKBN lm3_wssr (res * res)+ }+ where+ res = y - f++addLM3AccW :: LM3Acc -> Double -> Double -> V3 -> Double -> LM3Acc+addLM3AccW LM3Acc {..} y f (V3 d1 d2 d3) w = LM3Acc+ { lm3_n = lm3_n + 1+ , lm3_c11 = addKBN lm3_c11 (w * d1 * d1)+ , lm3_c12 = addKBN lm3_c12 (w * d1 * d2)+ , lm3_c13 = addKBN lm3_c12 (w * d1 * d3)+ , lm3_c22 = addKBN lm3_c22 (w * d2 * d2)+ , lm3_c23 = addKBN lm3_c22 (w * d2 * d3)+ , lm3_c33 = addKBN lm3_c22 (w * d3 * d3)+ , lm3_z1 = addKBN lm3_z1 (w * d1 * res)+ , lm3_z2 = addKBN lm3_z2 (w * d2 * res)+ , lm3_z3 = addKBN lm3_z3 (w * d3 * res)+ , lm3_wssr = addKBN lm3_wssr (w * res * res)+ }+ where+ res = y - f++-------------------------------------------------------------------------------+-- Output+-------------------------------------------------------------------------------++-- | Result of a curve fit.+data Fit v = Fit+ { fitParams :: !v -- ^ fit parameters+ , fitErrors :: !v -- ^ asympotic standard errors, /assuming a good fit/+ , fitNDF :: !Int -- ^ number of degrees of freedom+ , fitWSSR :: !Double -- ^ sum of squares of residuals+ }+ deriving Show++instance (NFData v) => NFData (Fit v) where+ rnf (Fit p e _ _) = rnf p `seq` rnf e++-------------------------------------------------------------------------------+-- LinRegAcc+-------------------------------------------------------------------------------++-- | Linear regression accumulator.+data LinRegAcc = LinRegAcc+ { lra_n :: {-# UNPACK #-} !Int -- ^ \(n\)+ , lra_w :: {-# UNPACK #-} !KBN -- ^ \(\sum w_i\)+ , lra_x :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i \)+ , lra_x2 :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i^2 \)+ , lra_y :: {-# UNPACK #-} !KBN -- ^ \(\sum y_i \)+ , lra_xy :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i y_i \)+ , lra_y2 :: {-# UNPACK #-} !KBN -- ^ \(\sum y_i^2 \)+ }+ deriving Show++instance NFData LinRegAcc where+ rnf LinRegAcc {} = ()++-- | All-zeroes 'LinRegAcc'.+zeroLinRegAcc :: LinRegAcc+zeroLinRegAcc = LinRegAcc 0 zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN++-- | Add a point to linreg accumulator.+addLinReg+ :: LinRegAcc+ -> Double -- ^ x+ -> Double -- ^ y+ -> LinRegAcc+addLinReg LinRegAcc {..} x y = LinRegAcc+ { lra_n = lra_n + 1+ , lra_w = addKBN lra_w 1+ , lra_x = addKBN lra_x x+ , lra_x2 = addKBN lra_x2 (x * x)+ , lra_y = addKBN lra_y y+ , lra_xy = addKBN lra_xy (x * y)+ , lra_y2 = addKBN lra_y2 (y * y)+ }++-- | Add a weighted point to linreg accumulator.+addLinRegW+ :: LinRegAcc+ -> Double -- ^ x+ -> Double -- ^ y+ -> Double -- ^ w+ -> LinRegAcc+addLinRegW LinRegAcc {..} x y w = LinRegAcc+ { lra_n = lra_n + 1+ , lra_w = addKBN lra_w w+ , lra_x = addKBN lra_x (w * x)+ , lra_x2 = addKBN lra_x2 (w * x * x)+ , lra_y = addKBN lra_y (w * y)+ , lra_xy = addKBN lra_xy (w * x * y)+ , lra_y2 = addKBN lra_y2 (w * y * y)+ }++-------------------------------------------------------------------------------+-- QuadRegAcc+-------------------------------------------------------------------------------++-- | Quadratic regression accumulator.+data QuadRegAcc = QuadRegAcc+ { qra_n :: {-# UNPACK #-} !Int -- ^ \(n\)+ , qra_w :: {-# UNPACK #-} !KBN -- ^ \(\sum w_i\)+ , qra_x :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i \)+ , qra_x2 :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i^2 \)+ , qra_x3 :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i^3 \)+ , qra_x4 :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i^4 \)+ , qra_y :: {-# UNPACK #-} !KBN -- ^ \(\sum y_i \)+ , qra_xy :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i y_i \)+ , qra_x2y :: {-# UNPACK #-} !KBN -- ^ \(\sum x_i^2 y_i \)+ , qra_y2 :: {-# UNPACK #-} !KBN -- ^ \(\sum y_i^2 \)+ }+ deriving Show++instance NFData QuadRegAcc where+ rnf QuadRegAcc {} = ()++-- | All-zeroes 'QuadRegAcc'.+zeroQuadRegAcc :: QuadRegAcc+zeroQuadRegAcc = QuadRegAcc 0 zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN zeroKBN++-- | Add a point to quadreg accumulator.+addQuadReg+ :: QuadRegAcc+ -> Double -- ^ x+ -> Double -- ^ y+ -> QuadRegAcc+addQuadReg QuadRegAcc {..} x y = QuadRegAcc+ { qra_n = qra_n + 1+ , qra_w = addKBN qra_w 1+ , qra_x = addKBN qra_x x+ , qra_x2 = addKBN qra_x2 x2+ , qra_x3 = addKBN qra_x3 (x * x2)+ , qra_x4 = addKBN qra_x4 (x2 * x2)+ , qra_y = addKBN qra_y y+ , qra_xy = addKBN qra_xy (x * y)+ , qra_x2y = addKBN qra_x2y (x2 * y)+ , qra_y2 = addKBN qra_y2 (y * y)+ }+ where+ x2 = x * x++-- | Add a weighted point to quadreg accumulator.+addQuadRegW+ :: QuadRegAcc+ -> Double -- ^ x+ -> Double -- ^ y+ -> Double -- ^ w+ -> QuadRegAcc+addQuadRegW QuadRegAcc {..} x y w = QuadRegAcc+ { qra_n = qra_n + 1+ , qra_w = addKBN qra_w w+ , qra_x = addKBN qra_x (w * x)+ , qra_x2 = addKBN qra_x2 (w * x2)+ , qra_x3 = addKBN qra_x3 (w * x * x2)+ , qra_x4 = addKBN qra_x4 (w * x2 * x2)+ , qra_y = addKBN qra_y (w * y)+ , qra_xy = addKBN qra_xy (w * x * y)+ , qra_x2y = addKBN qra_x2y (w * x2 * y)+ , qra_y2 = addKBN qra_y2 (w * y * y)+ }+ where+ x2 = x * x++-- | Convert 'QuadRegAcc' to 'LinRegAcc'.+--+-- Using this we can try quadratic and linear fits with a single data scan.+--+quadRegAccToLin :: QuadRegAcc -> LinRegAcc+quadRegAccToLin QuadRegAcc {..} = LinRegAcc+ { lra_n = qra_n+ , lra_w = qra_w+ , lra_x = qra_x+ , lra_x2 = qra_x2+ , lra_y = qra_y+ , lra_xy = qra_xy+ , lra_y2 = qra_y2+ }++-------------------------------------------------------------------------------+-- utils+-------------------------------------------------------------------------------++sq :: Num a => a -> a+sq x = x * x+{-# INLINE sq #-}++iterate1 :: (b -> b) -> b -> NE.NonEmpty b+iterate1 g x = NE.cons x (iterate1 g (g x))++eigenSM22 :: Double -> Double -> Double -> V2+eigenSM22 a b c = V2 ((ac + discr) / 2) ((ac - discr) / 2)+ where+ ac = a + c+ discr = sqrt (sq (a - c) + 4 * sq b)++-- | Levenberg-Marquard stop condition+lmStop :: Double -> Double -> Double -> Bool+lmStop lambda wssr wssr' =+ lambda < 1e-20 || lambda > 1e20 || isNaN wssr' || relDiff < 1e-10+ where+ relDiff = abs (wssr' - wssr) / wssr
+ src/Math/Regression/Simple/LinAlg.hs view
@@ -0,0 +1,402 @@+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+-- | Minimil linear algebra lib.+module Math.Regression.Simple.LinAlg (+ -- * Operations+ Add (..),+ Eye (..),+ Mult (..),+ Det (..),+ Inv (..),+ -- * Zeros+ zerosLin,+ zerosQuad,+ optimaQuad,+ -- * Two dimensions+ V2 (..),+ M22 (..),+ SM22 (..),+ -- * Three dimensions+ V3 (..),+ M33 (..),+ SM33 (..),+) where++import Control.DeepSeq (NFData (..))+import Data.Complex (Complex (..))++-------------------------------------------------------------------------------+-- Classes+-------------------------------------------------------------------------------++-- | Addition+class Add a where+ zero :: a+ add :: a -> a -> a++-- | Identity+class Eye a where+ eye :: a++-- | Multiplication of different things.+class Eye a => Mult a b c | a b -> c where+ mult :: a -> b -> c++-- | Determinant+class Eye a => Det a where+ det :: a -> Double++-- | Inverse+class Det a => Inv a where+ inv :: a -> a++infixl 6 `add`+infixl 7 `mult`++instance Eye Double where+ eye = 1++instance Add Double where+ zero = 0+ add = (+)++instance Mult Double Double Double where+ mult = (*)++instance Det Double where+ det = id++instance Inv Double where+ inv = recip++-------------------------------------------------------------------------------+-- Zeros+-------------------------------------------------------------------------------++-- | Solve linear equation.+--+-- >>> zerosLin (V2 1 2)+-- -2.0+--+zerosLin :: V2 -> Double+zerosLin (V2 a b) = negate (b / a)++-- | Solve quadratic equation.+--+-- >>> zerosQuad (V3 2 0 (-1))+-- Right (-0.7071067811865476,0.7071067811865476)+--+-- >>> zerosQuad (V3 2 0 1)+-- Left ((-0.0) :+ (-0.7071067811865476),(-0.0) :+ 0.7071067811865476)+--+-- Double root is not treated separately:+--+-- >>> zerosQuad (V3 1 0 0)+-- Right (-0.0,0.0)+--+-- >>> zerosQuad (V3 1 (-2) 1)+-- Right (1.0,1.0)+--+zerosQuad :: V3 -> Either (Complex Double, Complex Double) (Double, Double)+zerosQuad (V3 a b c)+ | delta < 0 = Left ((-b/da) :+ (-sqrtNDelta/da), (-b/da) :+ (sqrtNDelta/da))+ | otherwise = Right ((- b - sqrtDelta) / da, (-b + sqrtDelta) / da)+ where+ delta = b*b - 4 * a * c+ sqrtDelta = sqrt delta+ sqrtNDelta = sqrt (- delta)+ da = 2 * a++-- | Find an optima point.+--+-- >>> optimaQuad (V3 1 (-2) 0)+-- 1.0+--+-- compare to+--+-- >>> zerosQuad (V3 1 (-2) 0)+-- Right (0.0,2.0)+--+optimaQuad :: V3 -> Double+optimaQuad (V3 a b _) = zerosLin (V2 (2 * a) b)++-------------------------------------------------------------------------------+-- 2 dimensions+-------------------------------------------------------------------------------++-- | 2d vector. Strict pair of 'Double's.+--+-- Also used to represent linear polynomial: @V2 a b@ \(= a x + b\).+--+data V2 = V2 !Double !Double+ deriving (Eq, Show)++instance NFData V2 where+ rnf V2 {} = ()++instance Add V2 where+ zero = V2 0 0+ add (V2 x y) (V2 x' y') = V2 (x + x') (y + y')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Mult Double V2 V2 where+ mult k (V2 x y) = V2 (k * x) (k * y)+ {-# INLINE mult #-}++-- | 2×2 matrix.+data M22 = M22 !Double !Double !Double !Double+ deriving (Eq, Show)++instance NFData M22 where+ rnf M22 {} = ()++instance Add M22 where+ zero = M22 0 0 0 0+ add (M22 a b c d) (M22 a' b' c' d') = M22 (a + a') (b + b') (c + c') (d + d')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Eye M22 where+ eye = M22 1 0 0 1+ {-# INLINE eye #-}++instance Det M22 where det = det2+instance Inv M22 where inv = inv2++instance Mult Double M22 M22 where+ mult k (M22 a b c d) = M22 (k * a) (k * b) (k * c) (k * d)+ {-# INLINE mult #-}++instance Mult M22 V2 V2 where+ mult (M22 a b c d) (V2 u v) = V2 (a * u + b * v) (c * u + d * v)+ {-# INLINE mult #-}++-- | >>> M22 1 2 3 4 `mult` eye @M22+-- M22 1.0 2.0 3.0 4.0+instance Mult M22 M22 M22 where+ mult (M22 a b c d) (M22 x y z w) = M22+ (a * x + b * z) (a * y + b * w)+ (c * x + d * z) (c * y + d * w)+ {-# INLINE mult #-}++det2 :: M22 -> Double+det2 (M22 a b c d) = a * d - b * c+{-# INLINE det2 #-}++inv2 :: M22 -> M22+inv2 m@(M22 a b c d) = M22+ ( d / detm) (- b / detm)+ (- c / detm) ( a / detm)+ where+ detm = det2 m+{-# INLINE inv2 #-}++-- | Symmetric 2x2 matrix.+data SM22 = SM22 !Double !Double !Double+ deriving (Eq, Show)++instance NFData SM22 where+ rnf SM22 {} = ()++instance Add SM22 where+ zero = SM22 0 0 0+ add (SM22 a b d) (SM22 a' b' d') = SM22 (a + a') (b + b') (d + d')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Eye SM22 where+ eye = SM22 1 0 1+ {-# INLINE eye #-}++instance Det SM22 where det = detS2+instance Inv SM22 where inv = invS2++instance Mult Double SM22 SM22 where+ mult k (SM22 a b d) = SM22 (k * a) (k * b) (k * d)+ {-# INLINE mult #-}++instance Mult SM22 V2 V2 where+ mult (SM22 a b d) (V2 u v) = V2 (a * u + b * v) (b * u + d * v)+ {-# INLINE mult #-}++detS2 :: SM22 -> Double+detS2 (SM22 a b d) = a * d - b * b+{-# INLINE detS2 #-}++invS2 :: SM22 -> SM22+invS2 m@(SM22 a b d) = SM22+ ( d / detm)+ (- b / detm) ( a / detm)+ where+ detm = detS2 m+{-# INLINE invS2 #-}++-------------------------------------------------------------------------------+-- 3 dimensions+-------------------------------------------------------------------------------++-- | 3d vector. Strict triple of 'Double's.+--+-- Also used to represent quadratic polynomial: @V3 a b c@ \(= a x^2 + b x + c\).+data V3 = V3 !Double !Double !Double+ deriving (Eq, Show)++instance NFData V3 where+ rnf V3 {} = ()++instance Add V3 where+ zero = V3 0 0 0+ add (V3 x y z) (V3 x' y' z') = V3 (x + x') (y + y') (z + z')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Mult Double V3 V3 where+ mult k (V3 x y z) = V3 (k * x) (k * y) (k * z)+ {-# INLINE mult #-}++-- | 3×3 matrix.+data M33 = M33+ !Double !Double !Double+ !Double !Double !Double+ !Double !Double !Double+ deriving (Eq, Show)++instance NFData M33 where+ rnf M33 {} = ()++instance Add M33 where+ zero = M33 0 0 0 0 0 0 0 0 0++ add (M33 a b c d e f g h i) (M33 a' b' c' d' e' f' g' h' i') = M33+ (a + a') (b + b') (c + c')+ (d + d') (e + e') (f + f')+ (g + g') (h + h') (i + i')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Eye M33 where+ eye = M33 1 0 0+ 0 1 0+ 0 0 1+ {-# INLINE eye #-}++instance Det M33 where det = det3+instance Inv M33 where inv = inv3++instance Mult Double M33 M33 where+ mult k (M33 a b c d e f g h i) = M33+ (k * a) (k * b) (k * c)+ (k * d) (k * e) (k * f)+ (k * g) (k * h) (k * i)+ {-# INLINE mult #-}++instance Mult M33 V3 V3 where+ mult (M33 a b c+ d e f+ g h i) (V3 u v w) = V3+ (a * u + b * v + c * w)+ (d * u + e * v + f * w)+ (g * u + h * v + i * w)+ {-# INLINE mult #-}++-- TODO: instance Mult M33 M33 M33 where++det3 :: M33 -> Double+det3 (M33 a b c+ d e f+ g h i)+ = a * (e*i-f*h) - d * (b*i-c*h) + g * (b*f-c*e)+{-# INLINE det3 #-}++inv3 :: M33 -> M33+inv3 m@(M33 a b c+ d e f+ g h i)+ = M33 a' b' c'+ d' e' f'+ g' h' i'+ where+ a' = cofactor e f h i / detm+ b' = cofactor c b i h / detm+ c' = cofactor b c e f / detm+ d' = cofactor f d i g / detm+ e' = cofactor a c g i / detm+ f' = cofactor c a f d / detm+ g' = cofactor d e g h / detm+ h' = cofactor b a h g / detm+ i' = cofactor a b d e / detm+ cofactor q r s t = det2 (M22 q r s t)+ detm = det3 m+{-# INLINE inv3 #-}++-- | Symmetric 3×3 matrix.+data SM33 = SM33+ !Double+ !Double !Double+ !Double !Double !Double+ deriving (Eq, Show)++instance NFData SM33 where+ rnf SM33 {} = ()++instance Add SM33 where+ zero = SM33 0 0 0 0 0 0++ add (SM33 a d e g h i) (SM33 a' d' e' g' h' i') = SM33+ (a + a')+ (d + d') (e + e')+ (g + g') (h + h') (i + i')+ {-# INLINE zero #-}+ {-# INLINE add #-}++instance Eye SM33 where+ eye = SM33 1+ 0 1+ 0 0 1+ {-# INLINE eye #-}++instance Det SM33 where det = detS3+instance Inv SM33 where inv = invS3++instance Mult Double SM33 SM33 where+ mult k (SM33 a d e g h i) = SM33+ (k * a)+ (k * d) (k * e)+ (k * g) (k * h) (k * i)+ {-# INLINE mult #-}++instance Mult SM33 V3 V3 where+ mult (SM33 a+ d e+ g h i) (V3 u v w) = V3+ (a * u + d * v + g * w)+ (d * u + e * v + h * w)+ (g * u + h * v + i * w)+ {-# INLINE mult #-}++detS3 :: SM33 -> Double+detS3 (SM33 a+ d e+ g h i)+ = a * (e*i-h*h) - d * (d*i-g*h) + g * (d*h-g*e)+{-# INLINE detS3 #-}++invS3 :: SM33 -> SM33+invS3 m@(SM33 a+ d e+ g h i)+ = SM33 a'+ d' e'+ g' h' i'+ where+ a' = cofactor e h h i / detm+ d' = cofactor h d i g / detm+ e' = cofactor a g g i / detm+ g' = cofactor d e g h / detm+ h' = cofactor d a h g / detm+ i' = cofactor a d d e / detm+ cofactor q r s t = det2 (M22 q r s t)+ detm = detS3 m+{-# INLINE invS3 #-}
+ src/Numeric/KBN.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+-- | https://en.wikipedia.org/wiki/KBN_summation_algorithm+--+-- @math-functions@ has KBN-Babuška-Neumaier summation algorithm as well+-- in @Numeric.Sum@ module.+module Numeric.KBN (+ KBN (..),+ zeroKBN,+ getKBN,+ sumKBN,+ addKBN,+) where++import Control.DeepSeq (NFData (..))++import qualified Data.Foldable as F++-- $setup+-- >>> import qualified Data.List++-- | KBN summation accumulator.+data KBN = KBN !Double !Double+ deriving Show++instance NFData KBN where+ rnf KBN {} = ()++getKBN :: KBN -> Double+getKBN (KBN x e) = x + e++zeroKBN :: KBN+zeroKBN = KBN 0 0++-- | KBN summation algorithm.+--+-- >>> sumKBN (replicate 10 0.1)+-- 1.0+--+-- >>> Data.List.foldl' (+) 0 (replicate 10 0.1) :: Double+-- 0.9999999999999999+--+-- >>> sumKBN [1, 1e100, 1, -1e100]+-- 2.0+--+-- >>> Data.List.foldl' (+) 0 [1, 1e100, 1, -1e100]+-- 0.0+--+sumKBN :: F.Foldable f => f Double -> Double+sumKBN = sumKBNWith id++-- | Generalized version of 'sumKBN'.+sumKBNWith :: F.Foldable f => (a -> Double) -> f a -> Double+sumKBNWith f xs = getKBN (F.foldl' (\k a -> addKBN k (f a)) (KBN 0 0) xs)++-- | Add a 'Double' to 'KBN' accumulator.+addKBN :: KBN -> Double -> KBN+addKBN (KBN acc c) x = KBN acc' c' where+ acc' = acc + x+ c' | abs acc >= abs x = c + ((acc - acc') + x)+ | otherwise = c + ((x - acc') + acc)
+ test/generate-test-data.hs view
@@ -0,0 +1,60 @@+module Main where++import Control.Monad (unless)+import Data.List (unfoldr, zip5)+import System.Environment (getArgs)+import Text.Printf (printf)++import qualified System.Random.SplitMix as SM++main :: IO ()+main = do+ args <- getArgs+ unless (null args) $ do+ let (g1 : g2 : g3 : g4 : _) = unfoldr (Just . SM.splitSMGen) (SM.mkSMGen 42)++ writeFile "gnuplot/linear.dat" $ unlines+ [ printf "%9.05f %9.05f %9.05f %9.05f" (x + dx) (y + dy) devX devY+ | (x, devX, devY, px, py) <- zip5+ [ 1 .. 20 :: Double ]+ devsX+ devsY+ (normals (doubles g1))+ (normals (doubles g2))+ , let y = 3 * x + 5+ , let dx = devX * px+ , let dy = devY * py+ ]++ writeFile "gnuplot/quad.dat" $ unlines+ [ printf "%9.05f %9.05f %9.05f %9.05f" (x + dx) (y + dy) devX devY+ | (x, devX, devY, px, py) <- zip5+ [ 1 .. 20 :: Double ]+ devsX+ devsY+ (normals (doubles g3))+ (normals (doubles g4))+ , let y = 0.1 * x * x - 3 * x + 5+ , let dx = devX * px+ , let dy = devY * py+ ]++boxMuller :: Double -> Double -> (Double, Double)+boxMuller u v = (x, y) where+ x = k * sin (2 * pi * v)+ y = k * cos (2 * pi * u)+ k = sqrt (negate 2 * log u)++doubles :: SM.SMGen -> [Double]+doubles g = let (d, g') = SM.nextDouble g in d : doubles g'++normals :: [Double] -> [Double]+normals [] = []+normals [_] = []+normals (u:v:ds) = let (x, y) = boxMuller u v in x : y : normals ds++devsY :: [Double]+devsY = 1.2 : 1.4 : 1.6 : devsY++devsX :: [Double]+devsX = 0.2 : 0.3 : devsX
+ test/regression-simple-tests.hs view
@@ -0,0 +1,333 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveTraversable #-}+module Main (main) where++import Data.List (zip4)+import Test.Tasty (TestTree, defaultMain, testGroup, withResource)+import Test.Tasty.HUnit (assertEqual, testCase)++import qualified Data.Foldable as F+import qualified Data.List.NonEmpty as NE+import qualified Data.Traversable as T++#if __GLASGOW_HASKELL__ >= 704+import qualified Numeric.AD.Mode.Reverse.Double as AD+import qualified Statistics.Distribution as S+import qualified Statistics.Distribution.ChiSquared as S+#endif++import Math.Regression.Simple+import Numeric.KBN (sumKBN)++-------------------------------------------------------------------------------+-- Main+-------------------------------------------------------------------------------++main :: IO ()+main = defaultMain $ testGroup "regression-simple"+ [ linearTests+ , quadraticTests+ , lm1Tests+ , lm2Tests+ ]++-------------------------------------------------------------------------------+-- data+-------------------------------------------------------------------------------++withData :: FilePath -> (IO [(Double, Double, Double, Double)] -> TestTree) -> TestTree+withData fp = withResource acquire release where+ acquire = do+ contents <- readFile ("gnuplot/" ++ fp)+ return $ map (quad . map read . words) $ lines contents++ release _ = return ()++ quad :: [Double] -> (Double, Double, Double, Double)+ quad (x:y:dx:dy:_) = (x,y,dx,dy)+ quad _ = error "invalid data"++-------------------------------------------------------------------------------+-- Linear+-------------------------------------------------------------------------------++linearTests :: TestTree+linearTests = withData "linear.dat" $ \load -> testGroup "linear"+ [ testCase "no-errors" $ do+ linearData <- load+ let fit = linearFit (\(x,y,_,_) -> (x,y)) linearData+ assertEqual "params" (V2 2.95689 6.04617) (round' (fitParams fit))+ assertEqual "errors" (V2 7.9788e-2 0.95195) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 75.6356 (round' (fitWSSR fit))++ , testCase "y-errors" $ do+ linearData <- load+ let fit = linearWithYerrors (\(x,y,_,dy) -> (x,y,dy)) linearData+ assertEqual "params" (V2 2.97271 5.91878) (round' (fitParams fit))+ assertEqual "errors" (V2 7.722e-2 0.91882) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 38.8345 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 2.999e-3 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++ , testCase "xy-errors" $ do+ linearData <- load+ let fit = nth 5 $ linearWithXYerrors (\(x,y,dx,dy) -> (x,y,dx,dy)) linearData+ assertEqual "params" (V2 2.97021 5.99061) (round' (fitParams fit))+ assertEqual "errors" (V2 7.6542e-2 0.90917) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 29.141 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 4.6683e-2 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++ , testCase "yx-errors" $ do+ linearData <- load+ let fit = nth 5 $ linearWithXYerrors (\(x,y,dx,dy) -> (y,x,dy,dx)) linearData+ assertEqual "params" (V2 0.33271 (-1.87107)) (round' (fitParams fit))+ assertEqual "errors" (V2 8.5724e-3 0.34855) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 29.3171 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 4.4639e-2 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif+ ]++nth :: Int -> NE.NonEmpty a -> a+nth n (x NE.:| xs) = go n x xs where+ go _ z [] = z+ go m z (y:ys) = if m <= 0 then z else go (m - 1) y ys++-------------------------------------------------------------------------------+-- Quad+-------------------------------------------------------------------------------++quadraticTests :: TestTree+quadraticTests = withData "quad.dat" $ \load -> testGroup "quad"+ [ testCase "no-errors" $ do+ quadraticData <- load+ let fit = quadraticFit (\(x,y,_,_) -> (x,y)) quadraticData+ assertEqual "params" (V3 0.11487 (-3.34246) 6.63601) (round' (fitParams fit))+ assertEqual "errors" (V3 1.0297e-2 0.22674 1.07032) (round' (fitErrors fit))+ assertEqual "ndf" 17 (round' (fitNDF fit))+ assertEqual "wssr" 33.9104 (round' (fitWSSR fit))++ , testCase "y-errors" $ do+ quadraticData <- load+ let fit = quadraticWithYerrors (\(x,y,_,dy) -> (x,y,dy)) quadraticData+ assertEqual "params" (V3 0.11156 (-3.27481) 6.25286) (round' (fitParams fit))+ assertEqual "errors" (V3 9.7603e-3 0.21331 0.99362) (round' (fitErrors fit))+ assertEqual "ndf" 17 (round' (fitNDF fit))+ assertEqual "wssr" 16.793 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ let q = S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)+ assertEqual "P" 0.46847 (round' (1 - q))+#endif++ , testCase "xy-errors" $ do+ quadraticData <- load+ let fit = nth 5 $ quadraticWithXYerrors (\(x,y,dx,dy) -> (x,y,dx,dy)) quadraticData+ assertEqual "params" (V3 0.11222 (-3.29575) 6.39876) (round' (fitParams fit))+ assertEqual "errors" (V3 9.9372e-3 0.22027 1.06196) (round' (fitErrors fit))+ assertEqual "ndf" 17 (round' (fitNDF fit))+ assertEqual "wssr" 15.6318 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ let q = S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)+ assertEqual "P" 0.55007 (round' (1 - q))+#endif+ ]++-------------------------------------------------------------------------------+-- LM+-------------------------------------------------------------------------------++lm1Tests :: TestTree+lm1Tests = withData "linear.dat" $ \load -> testGroup "lm1"+ [ testCase "no-errors" $ do+ linearData <- load++ let scale a (x, y, _, _) = case scaleGrad' (H2 a x) of+ (f, H2 da _) -> (y, f, da)++ let fit = NE.last $ levenbergMarquardt1 scale 1 linearData+ assertEqual "params" 3.03374 (round' (fitParams fit))+ assertEqual "errors" 3.8628e-2 (round' (fitErrors fit))+ assertEqual "ndf" 19 (round' (fitNDF fit))+ assertEqual "wssr" 80.7105 (round' (fitWSSR fit))++ , testCase "y-errors" $ do+ linearData <- load+ let scaleY a (x, y, _, dy) = case scaleGrad' (H2 a x) of+ (f, H2 da _) -> (y, f, da, dy)+ let fit = NE.last $ levenbergMarquardt1WithYerrors scaleY 1 linearData+ assertEqual "params" 3.04015 (round' (fitParams fit))+ assertEqual "errors" 3.7604e-2 (round' (fitErrors fit))+ assertEqual "ndf" 19 (round' (fitNDF fit))+ assertEqual "wssr" 40.9918 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 2.4195e-3 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++ , testCase "xy-errors" $ do+ linearData <- load+ let scaleXY a (x, y, dx, dy) = case scaleGrad' (H2 a x) of+ (f, H2 da f') -> (y, f, da, f', dx, dy)+ let fit = NE.last $ levenbergMarquardt1WithXYerrors scaleXY 1 linearData+ assertEqual "params" 3.04315 (round' (fitParams fit))+ assertEqual "errors" 3.7477e-2 (round' (fitErrors fit))+ assertEqual "ndf" 19 (round' (fitNDF fit))+ assertEqual "wssr" 30.7021 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 4.3516e-2 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif+ ]++lm2Tests :: TestTree+lm2Tests = withData "linear.dat" $ \load -> testGroup "lm2"+ [ testCase "no-errors" $ do+ linearData <- load+ let lin (V2 a b) (x, y, _, _) = case linearGrad' (H3 a b x) of+ (f, H3 da db _) -> (y, f, V2 da db)++ let fit = NE.last $ levenbergMarquardt2 lin (V2 1 1) linearData+ assertEqual "params" (V2 2.95689 6.04617) (round' (fitParams fit))+ assertEqual "errors" (V2 7.9788e-2 0.95195) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 75.6356 (round' (fitWSSR fit))++ , testCase "y-errors" $ do+ linearData <- load+ let linY (V2 a b) (x, y, _, dy) = case linearGrad' (H3 a b x) of+ (f, H3 da db _) -> (y, f, V2 da db, dy)+ let fit = NE.last $ levenbergMarquardt2WithYerrors linY (V2 1 1) linearData+ assertEqual "params" (V2 2.97271 5.91882) (round' (fitParams fit))+ assertEqual "errors" (V2 7.722e-2 0.91882) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 38.8345 (round' (fitWSSR fit))++ , testCase "xy-errors" $ do+ linearData <- load+ let linXY (V2 a b) (x, y, dx, dy) = case linearGrad' (H3 a b x) of+ (f, H3 da db f') -> (y, f, V2 da db, f', dx, dy)+ let fit = NE.last $ levenbergMarquardt2WithXYerrors linXY (V2 1 1) linearData+ assertEqual "params" (V2 2.97021 5.99061) (round' (fitParams fit))+ assertEqual "errors" (V2 7.6542e-2 0.90917) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 29.141 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 4.6683e-2 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++ , testCase "yx-errors" $ do+ -- with x and y flipped:+ linearData <- load+ let linYX (V2 a b) (y, x, dy, dx) = case linearGrad' (H3 a b x) of+ (f, H3 da db f') -> (y, f, V2 da db, f', dx, dy)+ let fit = NE.last $ levenbergMarquardt2WithXYerrors linYX (V2 1 1) linearData+ assertEqual "params" (V2 0.33402 (-1.92156)) (round' (fitParams fit))+ assertEqual "errors" (V2 8.5785e-3 0.3488) (round' (fitErrors fit))+ assertEqual "ndf" 18 (round' (fitNDF fit))+ assertEqual "wssr" 29.1822 (round' (fitWSSR fit))++#if __GLASGOW_HASKELL__ >= 704+ assertEqual "P" 4.6197e-2 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++#if __GLASGOW_HASKELL__ >= 704+ , testCase "orear-example" $ do+ let orearData :: [(Double, Double, Double, Double)]+ orearData = zip4+ [22000, 22930,23880,25130,26390]+ [-4.017,-2.742,-1.1478,1.491,6.873]+ [440,470,500,530,540]+ [0.50,0.25,0.08,0.09,1.90]++ let orearXY (V2 a b) (x, y, dx, dy) = case AD.grad' orearF (H3 a b x) of+ (f, H3 da db f') -> (y, f, V2 da db, f', dx, dy)++ let wssr0 = sumKBN+ [ sq (y - f) * w+ | d <- orearData+ , let a1 = 1e-3+ , let a2 = 6e5+ , let (y, f, _, f', dx, dy) = orearXY (V2 a1 a2) d+ , let w = recip $ sq (f' * dx) + sq dy+ ]++ assertEqual "wssr0" 3.82243 (round' wssr0)++ let fit = NE.last $ levenbergMarquardt2WithXYerrors orearXY (V2 1e-3 6e5) orearData++ assertEqual "params" (V2 1.0163e-3 593725.0) (round' (fitParams fit))+ assertEqual "errors" (V2 1.7025e-4 95284.8) (round' (fitErrors fit))+ assertEqual "ndf" 3 (round' (fitNDF fit))+ assertEqual "wssr" 2.18668 (round' (fitWSSR fit))++ assertEqual "P" 0.53458 (round' (1 - S.cumulative (S.chiSquared (fitNDF fit)) (fitWSSR fit)))+#endif++ ]++data H2 a = H2 a a deriving (Functor, F.Foldable, T.Traversable)+data H3 a = H3 a a a deriving (Functor, F.Foldable, T.Traversable)++scaleF :: Num a => H2 a -> a+scaleF (H2 a x) = a * x + 5++scaleGrad' :: H2 Double -> (Double, H2 Double)+#if __GLASGOW_HASKELL__ >= 704+scaleGrad' = AD.grad' scaleF+#else+scaleGrad' (H2 a x) = (a * x + 5, H2 x a)+#endif++linearF :: Num a => H3 a -> a+linearF (H3 a b x) = a * x + b++linearGrad' :: H3 Double -> (Double, H3 Double)+#if __GLASGOW_HASKELL__ >= 704+linearGrad' = AD.grad' linearF+#else+linearGrad' (H3 a b x) = (a * x + b, H3 x 1 a)+#endif++orearF :: Fractional a => H3 a -> a+orearF (H3 a b x) = a * x - b / x++sq :: Num a => a -> a+sq x = x * x++-------------------------------------------------------------------------------+-- Round+-------------------------------------------------------------------------------++class Round a where+ round' :: a -> a++instance Round Double where+ round' 0 = 0+ round' x = fromInteger (round (x * rat)) / rat+ where+ mag = truncate (logBase 10 (abs x)) :: Int+ rat = 10 ^ (5 - mag)++instance Round Int where+ round' = id++instance Round V2 where+ round' (V2 x y) = V2 (round' x) (round' y)++instance Round V3 where+ round' (V3 x y z) = V3 (round' x) (round' y) (round' z)