hafar-0.1.1.0: src/Numeric/AffineForm/Subdivision.hs
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveFunctor #-}
-- | Subdivision contains an implementation of the branch-and-bound algorithm.
-- This method divides interval-like values into smaller subdivisions and then applies a function to those values.
-- This results in a more accurate output at the cost of having to repeat the calculations multiple times
module Numeric.AffineForm.Subdivision
( Subdivisible(..)
, SubdivisionEnvironment(..)
, defaultEnvironment
, branchAndBound
) where
import Control.Monad.Reader
import Control.Monad.State
import Data.List
import Numeric.Interval as IA
-- | The 'Subdivisible' class is used for datatypes that can be broken down into smaller pieces
-- and combined together
--
-- The 'subdivide' function subdivides the value into `n` smaller values and 'combine' joins
-- the subdivisions together into a bigger value
--
-- Subdividing and then combining a value is expected to give the initial value (sans rounding errors)
class Subdivisible a where
subdivide :: a -> Int -> [a]
combine2 :: a -> a -> a
combine :: [a] -> a
combine2 l r = combine [l,r]
combine = foldl1 combine2
instance (Ord a, Fractional a) => Subdivisible (Interval a) where
subdivide i n = f . fromIntegral <$> [1..n]
where f x = (((x-1)*w/n')IA....(x*w/n')) + (IA.singleton $ inf i)
w = width i
n' = fromIntegral n
combine2 = hull
instance (Subdivisible a) => Subdivisible [a] where
subdivide l n = sequenceA $ flip subdivide n <$> l
combine2 l r = uncurry combine2 <$> zip l r
-- | A data structure for configuring the 'branchAndBound' method
--
-- 'function' is the function that gets evaluated
-- 'errorFun' is the error measuring function of the result
-- 'maxError' specifies the maximum permitted error of an evaluation
-- 'maxDepth' specifies how deep the subdivision can go
-- 'subdivs' specifies the number of subdivisions per value
data SubdivisionEnvironment a b e = SubdivisionEnvironment
{ function :: a -> b
, errorFun :: b -> e
, maxError :: e
, maxDepth :: Int
, subdivs :: Int
}
-- | This function creates a simple subdivision configuration.
-- It requires the evaluator function and an error measuring function as its parameters.
defaultEnvironment :: (Fractional e) => (a -> b) -> (b -> e) -> SubdivisionEnvironment a b e
defaultEnvironment f g = SubdivisionEnvironment
{ function = f
, errorFun = g
, maxError = 0.1
, maxDepth = 3
, subdivs = 2
}
type Subdivider a b e = Reader (SubdivisionEnvironment a b e) (SubdivisionTree a)
data SubdivisionTree a
= Branch [SubdivisionTree a]
| Node a
deriving (Show, Functor)
deepen :: (Subdivisible a, Ord e) => SubdivisionTree a -> Int -> Subdivider a b e
deepen (Node x) depth =
do
env <- ask
let res = function env $ x
err = errorFun env $ res
n = subdivs env
if err <= maxError env
-- Accurate answer found
then return $ Node x
-- Subdivide and deepen
else deepen (Branch $ Node <$> subdivide x n) $ depth
deepen (Branch l) depth =
do
env <- ask
if depth >= maxDepth env
then return $ Branch l
else Branch <$> (sequence $ flip deepen (depth + 1) <$> l)
collapse :: (Subdivisible a) => SubdivisionTree a -> a
collapse (Node a) = a
collapse (Branch l) = combine $ collapse <$> l
evalTree :: (Subdivisible b) => Subdivider a b e -> SubdivisionEnvironment a b e -> b
evalTree div cfg = flip runReader cfg $
do
val <- div
env <- ask
return . collapse $ function env <$> val
-- | This function iteratively subdivides a value to arrive at a more accurate result
branchAndBound :: (Subdivisible a, Subdivisible b, Ord e) => a -> SubdivisionEnvironment a b e -> b
branchAndBound x env = flip evalTree env $ deepen (Node x) 0
test :: Interval Double
test =
flip evalTree env $ deepen (Node $ [3 IA.... 4, 1 IA....4]) 0
where env = SubdivisionEnvironment
{ function = (\[x, y] -> x * y - x)
, errorFun = width
, maxError = 0.1
, maxDepth = 5
, subdivs = 2
}