packages feed

dynobud-1.9.0.0: src/Dyno/Fitting.hs

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE PolyKinds #-}

module Dyno.Fitting
       ( l1Fit, l1Fits, withL1Fit
       , l2Fit, l2Fits, withL2Fit
       , lInfFit, lInfFits, withLInfFit
       , L1X(..), GSlacks(..)
       ) where

import GHC.Generics ( Generic )

import Casadi.MX ( MX )
import Casadi.Option ( Opt(..) )
import Casadi.Overloading ( ArcTan2 )
import qualified Data.Map as M
import Data.Vector ( Vector )
import Data.Proxy ( Proxy(..) )

import Dyno.Nlp ( Bounds, NlpOut(..) )
import Dyno.NlpSolver ( NlpSolver, withNlpSolver )
import Dyno.Solvers ( Solver )
import Dyno.Vectorize ( Vectorize, Id(..) )
import Dyno.TypeVecs ( Dim, Vec )
import qualified Dyno.TypeVecs as TV
import Dyno.View.Fun ( Fun, SXFun, call, toSXFun )
import Dyno.View.HList ( (:*:)(..) )
import Dyno.View.JVec ( JVec(..) )
import Dyno.View.M ( M, reshape, sumRows, trans, vcat, vsplit )
import Dyno.View.MapFun ( mapFun' )
import Dyno.View.View ( J, S, View(..), JTuple(..), JV, catJV, splitJV, jfill)

data L1X q n a =
  L1X (J (JV q) a) (J (JVec n (JV Id)) a)
  deriving Generic
instance (Vectorize q, Dim n) => View (L1X q n)

data GSlacks g n a =
  GSlacks (J (JV g) a) (J (JVec n (JV Id)) a) (J (JVec n (JV Id)) a)
  deriving Generic
instance (Vectorize g, Dim n) => View (GSlacks g n)


-- | Minimize the L1 norm of model mismatch.
--
-- > minimize:  || f(x_k, q) - y_k ||_1
-- >     q
-- >
-- > subject to:   qlb <=  q   <= qub
-- >               glb <= g(q) <= gub
--
-- reformulated as:
--
-- > minimize:              Sum(s_k)
-- >  q, s_k
-- > subject to:  qlb <=          q         <= qub
-- >              glb <=        g(q)        <= gub
-- >                     y_x - f(x_k) - s_k <= 0
-- >                0 <= y_x - f(x_k) + s_k
--
-- where q is the parameter vector, x_k are features, y_k are data,
-- and g is a nonlinear constraint on the parameters.
l1Fit ::
  forall n q g x
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> Maybe (q Double) -> q Bounds -> g Bounds -> M.Map String Opt
  -> Vec n (x Double, Double) -> IO (Either String (q Double))
l1Fit solver fitModel qConstraints mq0 qbnds gbnds mapOpts featuresData =
  unId <$> l1Fits solver fitModel qConstraints mapOpts (Id input)
  where
    input :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
    input = (mq0, qbnds, gbnds, featuresData)


-- | Solve multiple L1 fitting problems with the same structure.
-- This is equivilent to but more efficient than calling
-- 'l1Fit' many times.
l1Fits ::
  forall n q g x t
  . (Vectorize q, Vectorize g, Vectorize x, Traversable t, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> t (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
  -> IO (t (Either String (q Double)))
l1Fits solver fitModel qConstraints mapOpts inputs =
  withL1Fit solver fitModel qConstraints mapOpts (\fit -> mapM fit inputs)


-- | Low level interface to L1 fitting.
withL1Fit ::
  forall n q g x b
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> (((Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
       -> NlpSolver (L1X q n)
                    (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                    (GSlacks g n)
                    (Either String (q Double))
      ) -> NlpSolver (L1X q n)
                     (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                     (GSlacks g n)
                     b
     ) -> IO b
withL1Fit solver fitModel qConstraints mapOpts userFun = do
  let fitModel' (q :*: x :*: y :*: s) = f - y + s
        where
          f = vcat $ Id (fitModel (vsplit q) (vsplit x))

  fitModelFun <- toSXFun "fit_model" fitModel'
                 :: IO (SXFun
                        (J (JV q) :*: J (JV x) :*: S :*: S)
                        S
                       )

  mapFitModel <- mapFun' (Proxy :: Proxy n) "map_fit_model" fitModelFun mapOpts
                 :: IO (Fun
                        (J (JV q)
                         :*: M (JV x) (JVec n (JV Id))
                         :*: M (JV Id) (JVec n (JV Id))
                         :*: M (JV Id) (JVec n (JV Id))
                        )
                        (M (JV Id) (JVec n (JV Id)))
                       )
  let fg :: J (L1X q n) MX
            -> J (JTuple (JVec n (JV x)) (JVec n (JV Id))) MX
            -> (S MX, J (GSlacks g n) MX)
      fg dvs featuresData = (f, cat g)
        where
          fitFeatures :: J (JVec n (JV x)) MX
          fitData :: J (JVec n (JV Id)) MX
          JTuple fitFeatures fitData = split featuresData

          q :: J (JV q) MX
          s' :: J (JVec n (JV Id)) MX
          L1X q s' = split dvs

          s :: M (JV Id) (JVec n (JV Id)) MX
          s = trans s'

          ys :: M (JV Id) (JVec n (JV Id)) MX
          ys = trans fitData

          xs :: M (JV x) (JVec n (JV Id)) MX
          xs = reshape fitFeatures

          gs0 :: J (JVec n (JV Id)) MX
          gs0 = trans $ call mapFitModel (q :*: xs :*: ys :*: (-s))

          gs1 :: J (JVec n (JV Id)) MX
          gs1 = trans $ call mapFitModel (q :*: xs :*: ys :*: s)

          f = sumRows s'

          g :: GSlacks g n MX
          g = GSlacks (vcat (qConstraints (vsplit q))) gs0 gs1

  let action solveOne = userFun solveOne'
        where
          solveOne' :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
                      -> NlpSolver
                         (L1X q n)
                         (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                         (GSlacks g n)
                         (Either String (q Double))
          solveOne' (mq0, qbnds, gbnds', featuresData) =
            fmap (fmap toSol) (solveOne x0 p xbnds gbnds)
            where
              toSol out = splitJV xopt
                where
                  L1X xopt _ = split (xOpt out)

              p :: J (JTuple (JVec n (JV x)) (JVec n (JV Id))) (Vector Double)
              p = cat $ JTuple fs' ds'
                where
                  fitFeatures :: Vec n (x Double)
                  (fitFeatures, fitData) = TV.tvunzip featuresData
                  fs' = cat $ JVec $ fmap catJV fitFeatures
                  ds' = cat $ JVec $ fmap (catJV . Id) fitData
              xbnds :: J (L1X q n) (Vector Bounds)
              xbnds = cat $ L1X (catJV qbnds) (jfill (Nothing, Nothing))
              gbnds :: J (GSlacks g n) (Vector Bounds)
              gbnds = cat $ GSlacks (catJV gbnds')
                      (jfill (Nothing, Just 0)) (jfill ((Just 0, Nothing)))
              x0 :: J (L1X q n) (Vector Double)
              x0 = case mq0 of
                Nothing -> jfill 0
                Just q0 -> cat (L1X (catJV q0) (jfill 0))

  withNlpSolver solver fg Nothing Nothing Nothing Nothing action


-- | Minimize the L2 norm of model mismatch.
--
-- > minimize:  0.5 * || f(x_k, q) - y_k ||_2^2
-- >     q
-- >
-- > subject to:   qlb <=  q   <= qub
-- >               glb <= g(q) <= gub
--
-- where q is the parameter vector, x_k are features, y_k are data,
-- and g is a nonlinear constraint on the parameters.
l2Fit ::
  forall n q g x
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> Maybe (q Double) -> q Bounds -> g Bounds -> M.Map String Opt
  -> Vec n (x Double, Double) -> IO (Either String (q Double))
l2Fit solver fitModel qConstraints mq0 qbnds gbnds mapOpts featuresData = do
  unId <$> l2Fits solver fitModel qConstraints mapOpts (Id input)
  where
    input :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
    input = (mq0, qbnds, gbnds, featuresData)


-- | Solve multiple L2 fitting problems with the same structure.
-- This is equivilent to but more efficient than calling
-- 'l2Fit' many times.
l2Fits ::
  forall n q g x t
  . (Vectorize q, Vectorize g, Vectorize x, Traversable t, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> t (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
  -> IO (t (Either String (q Double)))
l2Fits solver fitModel qConstraints mapOpts inputs =
  withL2Fit solver fitModel qConstraints mapOpts (\fit -> mapM fit inputs)


-- | Low level interface to L2 fitting.
withL2Fit ::
  forall n q g x b
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> (((Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
       -> NlpSolver (JV q)
                    (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                    (JV g)
                    (Either String (q Double))
      ) -> NlpSolver (JV q)
                     (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                     (JV g)
                     b
     ) -> IO b
withL2Fit solver fitModel qConstraints mapOpts userFun = do
  let fitModel' (q :*: x :*: y) = err * err
        where
          err = f - y
          f = vcat $ Id (fitModel (vsplit q) (vsplit x))
  fitModelFun <- toSXFun "fit_model" fitModel'
                 :: IO (SXFun (J (JV q) :*: J (JV x) :*: S) S)

  mapFitModel <- mapFun' (Proxy :: Proxy n) "map_fit_model" fitModelFun mapOpts
                 :: IO (Fun
                        (J (JV q)
                         :*: M (JV x) (JVec n (JV Id))
                         :*: M (JV Id) (JVec n (JV Id))
                        )
                        S
                       )
  let fg :: J (JV q) MX -> J (JTuple (JVec n (JV x)) (JVec n (JV Id))) MX
            -> (S MX, J (JV g) MX)
      fg q featuresData = (0.5 * f, g)
        where
          fitFeatures :: J (JVec n (JV x)) MX
          fitData :: J (JVec n (JV Id)) MX
          JTuple fitFeatures fitData = split featuresData

          -- fit data
          ys :: M (JV Id) (JVec n (JV Id)) MX
          ys = trans fitData

          -- fit features
          xs :: M (JV x) (JVec n (JV Id)) MX
          xs = reshape fitFeatures

          -- objective function
          f :: S MX
          f = call mapFitModel (q :*: xs :*: ys)

          -- nonlinear parameter constraints
          g :: J (JV g) MX
          g = vcat (qConstraints (vsplit q))

  let action solveOne = userFun solveOne'
        where
          solveOne' :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
                      -> NlpSolver
                         (JV q)
                         (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                         (JV g)
                         (Either String (q Double))
          solveOne' (mq0, qbnds, gbnds', featuresData) =
            fmap (fmap (splitJV . xOpt)) (solveOne x0 p xbnds gbnds)
            where
              p :: J (JTuple (JVec n (JV x)) (JVec n (JV Id))) (Vector Double)
              p = cat $ JTuple fs' ds'
                where
                  fitFeatures :: Vec n (x Double)
                  (fitFeatures, fitData) = TV.tvunzip featuresData
                  fs' = cat $ JVec $ fmap catJV fitFeatures
                  ds' = cat $ JVec $ fmap (catJV . Id) fitData
              xbnds = catJV qbnds
              gbnds = catJV gbnds'
              x0 = case mq0 of
                Nothing -> jfill 0
                Just q0 -> catJV q0
  withNlpSolver solver fg Nothing Nothing Nothing Nothing action


-- | Minimize the L-infinity norm of model mismatch.
--
-- > minimize:  || f(x_k, q) - y_k ||_inf
-- >     q
-- >
-- > subject to:   qlb <=  q   <= qub
-- >               glb <= g(q) <= gub
--
-- reformulated as:
--
-- > minimize:                    s
-- >  q, s
-- > subject to:  qlb <=          q       <= qub
-- >              glb <=        g(q)      <= gub
-- >                     y_x - f(x_k) - s <= 0
-- >                0 <= y_x - f(x_k) + s
--
-- where q is the parameter vector, x_k are features, y_k are data,
-- and g is a nonlinear constraint on the parameters.
lInfFit ::
  forall n q g x
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> Maybe (q Double) -> q Bounds -> g Bounds -> M.Map String Opt
  -> Vec n (x Double, Double) -> IO (Either String (q Double))
lInfFit solver fitModel qConstraints mq0 qbnds gbnds mapOpts featuresData =
  unId <$> lInfFits solver fitModel qConstraints mapOpts (Id input)
  where
    input :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
    input = (mq0, qbnds, gbnds, featuresData)


-- | Solve multiple L-infinity fitting problems with the same structure.
-- This is equivilent to but more efficient than calling
-- 'lInfFit' many times.
lInfFits ::
  forall n q g x t
  . (Vectorize q, Vectorize g, Vectorize x, Traversable t, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> t (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
  -> IO (t (Either String (q Double)))
lInfFits solver fitModel qConstraints mapOpts inputs = do
  withLInfFit solver fitModel qConstraints mapOpts (\fit -> mapM fit inputs)


-- | Low-level interface to L-infinity fitting.
withLInfFit ::
  forall n q g x b
  . (Vectorize q, Vectorize g, Vectorize x, Dim n)
  => Solver
  -> (forall a . (Floating a, ArcTan2 a) => q a -> x a -> a)
  -> (forall a . (Floating a, ArcTan2 a) => q a -> g a)
  -> M.Map String Opt
  -> (((Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
       -> NlpSolver (JTuple (JV q) (JV Id))
                    (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                    (GSlacks g n)
                    (Either String (q Double))
      ) -> NlpSolver (JTuple (JV q) (JV Id))
                     (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                     (GSlacks g n)
                     b
     ) -> IO b
withLInfFit solver fitModel qConstraints mapOpts userFun = do
  let fitModel' (q :*: x :*: y :*: s) = f - y + s
        where
          f = vcat $ Id (fitModel (vsplit q) (vsplit x))

  fitModelFun <- toSXFun "fit_model" fitModel'
                 :: IO (SXFun
                        (J (JV q) :*: J (JV x) :*: S :*: S)
                        S
                       )

  mapFitModel <- mapFun' (Proxy :: Proxy n) "map_fit_model" fitModelFun mapOpts
                 :: IO (Fun
                        (J (JV q)
                         :*: M (JV x) (JVec n (JV Id))
                         :*: M (JV Id) (JVec n (JV Id))
                         :*: S
                        )
                        (M (JV Id) (JVec n (JV Id)))
                       )

  let fg :: J (JTuple (JV q) (JV Id)) MX
            -> J (JTuple (JVec n (JV x)) (JVec n (JV Id))) MX
            -> (S MX, J (GSlacks g n) MX)
      fg dvs featuresData = (s, cat g)
        where
          fitFeatures :: J (JVec n (JV x)) MX
          fitData :: J (JVec n (JV Id)) MX
          JTuple fitFeatures fitData = split featuresData

          q :: J (JV q) MX
          s :: S MX
          JTuple q s = split dvs

          ys :: M (JV Id) (JVec n (JV Id)) MX
          ys = trans fitData

          xs :: M (JV x) (JVec n (JV Id)) MX
          xs = reshape fitFeatures

          gs0 :: J (JVec n (JV Id)) MX
          gs0 = trans $ call mapFitModel (q :*: xs :*: ys :*: (-s))

          gs1 :: J (JVec n (JV Id)) MX
          gs1 = trans $ call mapFitModel (q :*: xs :*: ys :*: s)

          g :: GSlacks g n MX
          g = GSlacks (vcat (qConstraints (vsplit q))) gs0 gs1

  let action solveOne = userFun solveOne'
        where
          solveOne' :: (Maybe (q Double), q Bounds, g Bounds, Vec n (x Double, Double))
                      -> NlpSolver
                         (JTuple (JV q) (JV Id))
                         (JTuple (JVec n (JV x)) (JVec n (JV Id)))
                         (GSlacks g n)
                         (Either String (q Double))
          solveOne' (mq0, qbnds, gbnds', featuresData) =
            fmap (fmap toSol) (solveOne x0 p xbnds gbnds)
            where
              toSol out = splitJV xopt
                where
                  JTuple xopt _ = split (xOpt out)

              p :: J (JTuple (JVec n (JV x)) (JVec n (JV Id))) (Vector Double)
              p = cat $ JTuple fs' ds'
                where
                  fitFeatures :: Vec n (x Double)
                  (fitFeatures, fitData) = TV.tvunzip featuresData
                  fs' = cat $ JVec $ fmap catJV fitFeatures
                  ds' = cat $ JVec $ fmap (catJV . Id) fitData
              xbnds = cat $ JTuple (catJV qbnds) (catJV (Id (Nothing, Nothing)))
              gbnds = cat $ GSlacks (catJV gbnds')
                      (jfill (Nothing, Just 0)) (jfill (Just 0, Nothing))
              x0 = case mq0 of
                Nothing -> jfill 0
                Just q0 -> cat (JTuple (catJV q0) (catJV (Id 0)))

  withNlpSolver solver fg Nothing Nothing Nothing Nothing action