brush-strokes-0.1.0.0: src/lib/Math/Taylor.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
module Math.Taylor where
-- base
import Prelude
hiding ( Num(..), (/), fromRational )
import Control.Monad.ST
( ST, runST )
import Data.Kind
( Type, Constraint )
import Data.Ratio
( (%) )
import Data.Semigroup
( Product(..) )
import GHC.Exts
( Double(D#), IsList(..)
, proxy#, fmaddDouble#
)
import GHC.TypeNats
( Nat, KnownNat
, natVal'
, type (+), type (-)
)
-- primitive
import qualified Data.Primitive.Array as Prim
import qualified Data.Primitive.PrimArray as Prim
-- brush-strokes
import Math.Interval
( π(π), πβ
, hull, addWithErr, subWithErr, prodWithErr, scaleInterval
)
import Math.Linear
( β, Fin (..), Representable (index) )
import Math.Ring
import Math.Root.Isolation.Core
( forEachCoord )
--------------------------------------------------------------------------------
-- | The shape of a tensor of the given order.
type Shape :: Nat -> Type
data Shape order where
Nil :: Shape 0
(:.) :: Nat -> Shape ( n - 1 ) -> Shape n
type KnownShape :: forall {order}. Shape order -> Constraint
class KnownShape shape where
knownShape :: [ Int ]
instance KnownShape Nil where
knownShape = []
instance ( KnownNat n, KnownShape sh ) => KnownShape ( n :. sh ) where
knownShape = fromIntegral ( natVal' @n proxy# ) : knownShape @sh
nbCoefficients :: forall sh. KnownShape sh => Int
nbCoefficients = getProduct $ foldMap ( Product . nbCoeffs ) ( knownShape @sh )
where
-- A degree d polynomial has (d+1) coefficients.
nbCoeffs :: Int -> Int
nbCoeffs deg = deg + 1
-- | A Taylor model in Bernstein form, consisting of a multivariate polynomial
-- given as a Bernstein tensor + an error term (interval).
--
-- Invariant: the interval always contains @0@ (although it is not necessarily
-- symmetric around @0@).
type BernsteinModel :: Shape order -> Type
data BernsteinModel degs
= BernsteinModel
{ bernsteinCoefficients :: !( Prim.PrimArray Double )
-- Tensor coefficients, stored in row-major order.
, bernsteinModelRemainder :: !π
}
instance KnownShape degs => AbelianGroup ( BernsteinModel degs ) where
BernsteinModel c1 e1 + BernsteinModel c2 e2 =
let (fpErr, cNew) = zipWithCoeffs addWithErr c1 c2
in BernsteinModel cNew (e1 + e2 + fpErr)
BernsteinModel c1 e1 - BernsteinModel c2 e2 =
let (fpErr, cNew) = zipWithCoeffs subWithErr c1 c2
in BernsteinModel cNew (e1 + e2 + fpErr)
negate ( BernsteinModel coeffs err ) =
-- Floating-point negation is exact, so no change in error.
BernsteinModel ( Prim.mapPrimArray negate coeffs ) ( negate err )
fromInteger i =
case fromInteger i of
π lo hi ->
if lo == hi
then
BernsteinModel
( Prim.replicatePrimArray ( nbCoefficients @degs ) ( fromIntegral i ) )
0
else
let !x = 0.5 * ( lo + hi )
in
BernsteinModel
( Prim.replicatePrimArray ( nbCoefficients @degs ) x )
( π ( lo - x ) ( hi - x ) )
-- | Bernstein coefficient strides (assuming a row-major layout).
getStrides :: [ Int ] -> [ Int ]
getStrides shape =
let degs = map (+1) shape
in drop 1 $ scanr (*) 1 degs
choose :: Int -> Int -> Integer
choose n k
| k < 0 || k > n = 0
| k == 0 || k == n = 1
| k > n `div` 2 = choose n (n - k)
| otherwise = go 1 1
where
n', k' :: Integer
n' = fromIntegral n
k' = fromIntegral k
go :: Integer -> Integer -> Integer
go acc i
| i > k' = acc
| otherwise = go (acc * (n' - i + 1) `div` i) (i + 1)
-- | Compute the Bernstein weight \( \frac { { n \choose i } { m \choose j } }{ { {n + m} \choose { i + j } } } \).
bernsteinWeight :: Int -> Int -> Int -> Int -> π
bernsteinWeight n m i j =
let
-- Compute the exact rational using integer arithmetic,
-- then use 'fromRational'. This avoids any accumulation of floating-point
-- errors, at the cost of using large integers.
num = choose n i * choose m j
den = choose (n + m) (i + j)
in
-- NB: the 'fromRational' implementation for π produces an enclosure of
-- the exact rational number by 'Double's.
fromRational $ num % den
-- | Helper function for computing Bernstein weights in the computation of
-- multiplication of two Bernstein models.
bernsteinWeights
:: forall { order } ( degs1 :: Shape order ) ( degs2 :: Shape order )
. ( KnownNat order, KnownShape degs1, KnownShape degs2 )
=> ( Prim.Array ( Prim.PrimArray Double )
, Prim.Array ( Prim.PrimArray π )
)
bernsteinWeights =
let
!dims1 = fromList $ knownShape @degs1
!dims2 = fromList $ knownShape @degs2
!order = fromIntegral $ natVal' @order proxy#
in runST \ @s -> do
marrBoxedVal <- Prim.newArray @( ST s ) @( Prim.PrimArray Double ) order undefined
marrBoxedErr <- Prim.newArray @( ST s ) @( Prim.PrimArray π ) order undefined
forLoop 0 order \ d -> do
let
n = Prim.indexPrimArray dims1 d
m = Prim.indexPrimArray dims2 d
rowSize = m + 1
totalSize = (n + 1) * rowSize
marrWeightsVal <- Prim.newPrimArray @( ST s ) @Double totalSize
marrWeightsErr <- Prim.newPrimArray @( ST s ) @π totalSize
forLoop 0 ( n + 1 ) \ i -> do
let
!offsetI = i * rowSize
forLoop 0 ( m + 1 ) \ j -> do
let
!( π lo hi ) = bernsteinWeight n m i j
!val = 0.5 * ( lo + hi )
!err = π ( lo - val ) ( hi - val )
Prim.writePrimArray marrWeightsVal (offsetI + j) val
Prim.writePrimArray marrWeightsErr (offsetI + j) err
frozenVal <- Prim.unsafeFreezePrimArray marrWeightsVal
frozenErr <- Prim.unsafeFreezePrimArray marrWeightsErr
Prim.writeArray marrBoxedVal d frozenVal
Prim.writeArray marrBoxedErr d frozenErr
weightsVal <- Prim.unsafeFreezeArray marrBoxedVal
weightsErr <- Prim.unsafeFreezeArray marrBoxedErr
return ( weightsVal, weightsErr )
type ZipSum :: Shape order -> Shape order -> Shape order
type family ZipSum as bs where
ZipSum Nil Nil = Nil
ZipSum (a :. as) (b :. bs) = (a + b) :. ZipSum as bs
-- | Multiply two Bernstein models (without any degree reduction).
timesBernsteinModel
:: forall { order } ( degs1 :: Shape order ) ( degs2 :: Shape order )
. ( KnownNat order
, KnownShape degs1, KnownShape degs2
, KnownShape ( ZipSum degs1 degs2 )
)
=> BernsteinModel degs1
-> BernsteinModel degs2
-> BernsteinModel ( ZipSum degs1 degs2 )
timesBernsteinModel =
let
-- 1. Setup constants
!env = ConvolutionEnv
{ envOrder = fromIntegral $ natVal' @order proxy#
, envDims1 = fromList $ knownShape @degs1
, envDims2 = fromList $ knownShape @degs2
, envDimsOut = fromList $ knownShape @( ZipSum degs1 degs2 )
, envStrides1 = fromList $ getStrides $ knownShape @degs1
, envStrides2 = fromList $ getStrides $ knownShape @degs2
, envStridesOut = fromList $ getStrides $ knownShape @( ZipSum degs1 degs2 )
, envWeightsVal = fst $ bernsteinWeights @degs1 @degs2
, envWeightsErr = snd $ bernsteinWeights @degs1 @degs2
}
!nbOut = nbCoefficients @( ZipSum degs1 degs2 )
in
\ (BernsteinModel coeffs1 err1) (BernsteinModel coeffs2 err2) ->
runST $ do
-- 2. Initialise output coefficient array & output FP-error array
marrVal <- Prim.newPrimArray nbOut
marrErr <- Prim.newPrimArray nbOut
Prim.setPrimArray marrVal 0 nbOut 0
Prim.setPrimArray marrErr 0 nbOut 0
-- 3. Run the gathering weighted convolution
runOutputWalker env coeffs1 coeffs2 marrVal marrErr
-- 4. Compute the final error term and finish.
resultCoeffs <- Prim.unsafeFreezePrimArray marrVal
fpErrs <- Prim.unsafeFreezePrimArray marrErr
let
!( π min1 max1 ) = minMaxCoeffs coeffs1
!( π min2 max2 ) = minMaxCoeffs coeffs2
!bound1 = max (abs min1) (abs max1)
!bound2 = max (abs min2) (abs max2)
!properErr = scaleInterval bound2 err1 + scaleInterval bound1 err2 + err1 * err2
!finalErr = properErr + hulls fpErrs
return $ BernsteinModel resultCoeffs finalErr
{-# INLINEABLE timesBernsteinModel #-}
data ConvolutionEnv = ConvolutionEnv
{ envOrder :: !Int
, envDims1 :: !( Prim.PrimArray Int )
, envDims2 :: !( Prim.PrimArray Int )
, envDimsOut :: !( Prim.PrimArray Int )
, envStrides1 :: !( Prim.PrimArray Int )
, envStrides2 :: !( Prim.PrimArray Int )
, envStridesOut :: !( Prim.PrimArray Int )
, envWeightsVal :: !( Prim.Array ( Prim.PrimArray Double ) )
, envWeightsErr :: !( Prim.Array ( Prim.PrimArray π ) )
}
data FiberBuffer s = FiberBuffer
{ fbVal :: !( Prim.MutablePrimArray s Double )
, fbErr :: !( Prim.MutablePrimArray s π )
}
-- | Lightweight grouping for the current traversal state.
data Cursor = Cursor
{ curOff1 :: !Int -- ^ Current offset in input 1
, curOff2 :: !Int -- ^ Current offset in input 2
, curWVal :: !Double -- ^ Accumulated Bernstein weight product
, curWErr :: !π -- ^ Accumulated weight error
}
runOutputWalker
:: forall s
. ConvolutionEnv
-> Prim.PrimArray Double -- ^ Coeffs 1
-> Prim.PrimArray Double -- ^ Coeffs 2
-> Prim.MutablePrimArray s Double -- ^ Output Coeffs
-> Prim.MutablePrimArray s π -- ^ Output Errors
-> ST s ()
{-# INLINE runOutputWalker #-}
runOutputWalker env c1 c2 marrVal marrErr =
if envOrder env == 0
then do
-- Trivial scalar codepath
let !val1 = Prim.indexPrimArray c1 0
!val2 = Prim.indexPrimArray c2 0
!(# pVal, pErr #) = weightedProduct val1 val2 1 0
oldVal <- Prim.readPrimArray marrVal 0
oldErr <- Prim.readPrimArray marrErr 0
let !(# newVal, sumErr #) = addWithErr oldVal pVal
!newErr = oldErr + pErr + sumErr
Prim.writePrimArray marrVal 0 newVal
Prim.writePrimArray marrErr 0 newErr
else do
let lastDimLen = Prim.indexPrimArray (envDimsOut env) (envOrder env - 1)
-- Allocate reusable buffers for the fiber dimension
tmpVal <- Prim.newPrimArray lastDimLen
tmpErr <- Prim.newPrimArray lastDimLen
let fiberBuf = FiberBuffer tmpVal tmpErr
-- Array to track output indices (k) for prefix dimensions
kIndices <- Prim.newPrimArray (envOrder env)
-- Iterate over every output fiber
iterateOutputFibers env kIndices 0 0 $ \outputOffset -> do
-- A. Reset the temporary accumulation buffer
Prim.setPrimArray tmpVal 0 lastDimLen 0
Prim.setPrimArray tmpErr 0 lastDimLen 0
-- B. Accumulate all input contributions into the buffer
let startCursor = Cursor 0 0 1 0
accumulateFiberInputs env c1 c2 kIndices fiberBuf 0 startCursor
-- C. Write the fiber into the main output array
commitFiber marrVal marrErr fiberBuf outputOffset
-- | Iterates over all output indices for dimensions 0 to N-2.
-- Calls the 'action' callback when a fiber (dimension N-1) is identified.
iterateOutputFibers
:: ConvolutionEnv
-> Prim.MutablePrimArray s Int -- ^ current output indices
-> Int -- ^ current depth
-> Int -- ^ current output offset
-> ( Int -> ST s () ) -- ^ action to run on the fiber (receives offset)
-> ST s ()
{-# INLINE iterateOutputFibers #-}
iterateOutputFibers env kIndices depth offOut action
| depth == envOrder env - 1 = action offOut
| otherwise = do
let
!outDeg = Prim.indexPrimArray (envDimsOut env) depth
!stride = Prim.indexPrimArray (envStridesOut env) depth
-- Loop over k for this dimension
forLoop 0 ( outDeg + 1 ) \ k -> do
Prim.writePrimArray kIndices depth k
iterateOutputFibers env kIndices (depth + 1) (offOut + k * stride) action
-- | Finds all (i, j) pairs such that i + j = k for the prefix dimensions.
-- Updates the Cursor (offsets and weights) and recurses.
accumulateFiberInputs
:: ConvolutionEnv
-> Prim.PrimArray Double -- ^ argument 1 coefficients
-> Prim.PrimArray Double -- ^ argument 2 coefficients
-> Prim.MutablePrimArray s Int -- ^ output indices
-> FiberBuffer s
-> Int -- ^ current depth
-> Cursor -- ^ current accumulated state (offsets/weights)
-> ST s ()
{-# INLINE accumulateFiberInputs #-}
accumulateFiberInputs env c1 c2 kIndices buf depth cur
-- Base case: run the 1D convolution.
| depth == envOrder env - 1 =
weightedConvolve1D env c1 c2 buf cur
-- Recursive Step: match i + j = k for this dimension
| otherwise = do
targetK <- Prim.readPrimArray kIndices depth
let
!n = Prim.indexPrimArray (envDims1 env) depth
!m = Prim.indexPrimArray (envDims2 env) depth
!s1 = Prim.indexPrimArray (envStrides1 env) depth
!s2 = Prim.indexPrimArray (envStrides2 env) depth
-- Bernstein weights for this dimension
!wArrVal = Prim.indexArray (envWeightsVal env) depth
!wArrErr = Prim.indexArray (envWeightsErr env) depth
!wRowLen = m + 1
-- Valid range for i
!startI = max 0 (targetK - m)
!endI = min n targetK
forLoop startI (endI + 1) \i -> do
let
!j = targetK - i
!wIdx = (i * wRowLen) + j
-- Fetch weight for this (i, j) pair
!dimWVal = Prim.indexPrimArray wArrVal wIdx
!dimWErr = Prim.indexPrimArray wArrErr wIdx
-- Update accumulated weight via product: wNew = wOld * wDim
!(# nextWVal, wProdErr #) = prodWithErr (curWVal cur) dimWVal
!nextWErr = scaleInterval (curWVal cur) dimWErr
+ scaleInterval dimWVal (curWErr cur)
+ (curWErr cur) * dimWErr
+ wProdErr
-- Update offsets
!nextCursor =
Cursor
{ curOff1 = curOff1 cur + (i * s1)
, curOff2 = curOff2 cur + (j * s2)
, curWVal = nextWVal
, curWErr = nextWErr
}
accumulateFiberInputs env c1 c2 kIndices buf (depth + 1) nextCursor
-- | Performs a weighted convolution for the innermost dimension and adds
-- the result into the FiberBuffer.
weightedConvolve1D
:: ConvolutionEnv
-> Prim.PrimArray Double
-> Prim.PrimArray Double
-> FiberBuffer s
-> Cursor
-> ST s ()
{-# INLINE weightedConvolve1D #-}
weightedConvolve1D env coeffs1 coeffs2 (FiberBuffer bufVal bufErr) (Cursor off1 off2 wVal wErr) = do
let
!lastDim = envOrder env - 1
!n = Prim.indexPrimArray (envDims1 env) lastDim
!m = Prim.indexPrimArray (envDims2 env) lastDim
!s1 = Prim.indexPrimArray (envStrides1 env) lastDim
!s2 = Prim.indexPrimArray (envStrides2 env) lastDim
!wArrVal = Prim.indexArray (envWeightsVal env) lastDim
!wArrErr = Prim.indexArray (envWeightsErr env) lastDim
!wRowLen = m + 1
!outDeg = n + m
-- Standard 1D convolution
forLoop 0 (outDeg + 1) \k -> do
let
!startI = max 0 (k - m)
!endI = min n k
-- Inner reduction loop: sum( c1[i]*c2[j]*w[i,j] )
!(fiberVal, fiberErr) =
foldLoop startI (endI + 1) (0, 0) \i (accVal, accErr) ->
let
!j = k - i
!c1 = Prim.indexPrimArray coeffs1 (off1 + i * s1)
!c2 = Prim.indexPrimArray coeffs2 (off2 + j * s2)
!wIdx = i * wRowLen + j
!dimWVal = Prim.indexPrimArray wArrVal wIdx
!dimWErr = Prim.indexPrimArray wArrErr wIdx
-- Compute term: (c1 * c2) * w
!(# p, pErr #) = prodWithErr c1 c2
!(# termVal, termProdErr #) = prodWithErr p dimWVal
!termErr = scaleInterval p dimWErr
+ scaleInterval dimWVal pErr
+ pErr * dimWErr
+ termProdErr
!(# nextAcc, sumErr #) = addWithErr accVal termVal
!nextAccErr = accErr + termErr + sumErr
in (nextAcc, nextAccErr)
-- Apply the prefix weight (wVal) accumulated from higher dimensions.
-- result = fiber_sum * prefix_weight
let !(# finalVal, finalProdErr #) = prodWithErr fiberVal wVal
!finalErr = scaleInterval fiberVal wErr
+ scaleInterval wVal fiberErr
+ wErr * fiberErr
+ finalProdErr
-- Accumulate into temporary buffer
oldBufVal <- Prim.readPrimArray bufVal k
oldBufErr <- Prim.readPrimArray bufErr k
let !(# newBufVal, bufSumErr #) = addWithErr oldBufVal finalVal
!newBufErr = oldBufErr + finalErr + bufSumErr
Prim.writePrimArray bufVal k newBufVal
Prim.writePrimArray bufErr k newBufErr
commitFiber
:: Prim.MutablePrimArray s Double
-> Prim.MutablePrimArray s π
-> FiberBuffer s
-> Int
-> ST s ()
{-# INLINE commitFiber #-}
commitFiber marrVal marrErr ( FiberBuffer bufVal bufErr ) outputOffset = do
len <- Prim.getSizeofMutablePrimArray bufVal
Prim.copyMutablePrimArray marrVal outputOffset bufVal 0 len
Prim.copyMutablePrimArray marrErr outputOffset bufErr 0 len
weightedProduct
:: Double -- ^ coefficient from tensor 1
-> Double -- ^ coefficient from tensor 2
-> Double -- ^ accumulated weight
-> π -- ^ accumulated weight error
-> (# Double, π #)
{-# INLINE weightedProduct #-}
weightedProduct c1 c2 wVal wErr =
let
-- Compute ( c1 * c2 ) * w, keeping track of FP error.
!(# p, pErr #) = prodWithErr c1 c2
!(# termVal, termProdErr #) = prodWithErr p wVal
!termErr = scaleInterval p wErr
+ scaleInterval wVal pErr
+ pErr * wErr
+ termProdErr
in
(# termVal, termErr #)
fma :: Double -> Double -> Double -> Double
fma ( D# x ) ( D# y ) ( D# z ) = D# ( fmaddDouble# x y z )
boundingBox :: forall order ( degs :: Shape order ). BernsteinModel degs -> π
boundingBox ( BernsteinModel coeffs err ) =
err + minMaxCoeffs coeffs
minMaxCoeffs :: Prim.PrimArray Double -> π
minMaxCoeffs arr = loop 0 $ π (1/0) (-1/0)
where
len = Prim.sizeofPrimArray arr
loop !i ival@( π mn mx )
| i >= len = ival
| otherwise =
let !x = Prim.indexPrimArray arr i
in loop (i+1) $ π (min mn x) (max mx x)
hulls :: Prim.PrimArray π -> π
hulls arr = loop 0 $ π (1/0) (-1/0)
where
len = Prim.sizeofPrimArray arr
loop !i ival
| i >= len = ival
| otherwise =
let !x = Prim.indexPrimArray arr i
in loop (i+1) $ hull x ival
refineBernsteinModel
:: forall { order } ( degs :: Shape order )
. ( KnownNat order
, KnownShape degs
, Representable π ( πβ order )
)
=> πβ order -- ^ The domains @[u_i, v_i]@ for each dimension
-> BernsteinModel degs
-> BernsteinModel degs
refineBernsteinModel =
let
!dims = knownShape @degs
!maxDeg = if null dims then 0 else maximum dims
!strides = getStrides dims
in \ domains ( BernsteinModel coeffs inputErr ) -> runST \ @s -> do
-- Allocate a scratch buffer for 'restrict1D'
scratchBuf <- Prim.newPrimArray @( ST s ) @Double ( maxDeg + 1 )
-- 1. Copy coefficients to a mutable array (we modify in place)
let len = Prim.sizeofPrimArray coeffs
marr <- Prim.newPrimArray len
Prim.copyPrimArray marr 0 coeffs 0 len
let
-- Iterate over all 1D lines for a specific dimension 'd'
-- This loops over all indices *except* dimension 'd'.
applyToDim :: Fin order -> π -> π -> ST s π
applyToDim ( Fin d_one_ixed ) ( π u v ) currentAccErr = do
let
-- Fin uses 1-based indexing (not for any good reason), so account for that
!d = fromIntegral $ d_one_ixed - 1
!n = dims !! d
!stride = strides !! d
!dimSize = n + 1
!block = stride * dimSize
loopBlock !base !acc
| base >= len = return acc
| otherwise = do
let loopOffset !off !innerAcc
| off >= stride = return innerAcc
| otherwise = do
let !idx = base + off
e <- restrict1D scratchBuf ( Strided marr idx stride ) n (π u v)
loopOffset (off + 1) (innerAcc + e)
errChunk <- loopOffset 0 0
loopBlock (base + block) (acc + errChunk)
loopBlock 0 currentAccErr
-- 2. Apply refinement dimension by dimension
totalRefinementErr <-
forEachCoord @order 0 \ d accErr -> do
let !dom = domains `index` d
applyToDim d dom accErr
finalCoeffs <- Prim.unsafeFreezePrimArray marr
-- 3. Update error
-- The accumulated floating point error from De Casteljau must be added.
return $ BernsteinModel finalCoeffs (inputErr + totalRefinementErr)
-- | Restricts a 1D Bernstein polynomial from domain [0,1] to [u,v].
--
-- Returns the floating-point error incurred during the transformation.
restrict1D
:: forall s
. Prim.MutablePrimArray s Double -- ^ scratch buffer (size >= degree + 1)
-> Strided s -- coefficient buffer
-> Int -- ^ degree
-> π
-> ST s π
restrict1D buf coeffs deg ( π u v ) = do
-- Use the passed-in scratch buffer for the computaiton.
let !count = deg + 1
forLoop 0 count \ k -> do
val <- readStrided coeffs k
Prim.writePrimArray buf k val
-- Do right Casteljau on our local buffer, [0,1] --> [u,0].
err1 <-
if u == 0
then return 0
else deCasteljauRight buf deg u
-- Do left Casteljau on our local buffer, [u,0] --> [u,v].
-- As we have already re-scaled once, the new right edge is not v but ( v - u ) / ( 1 - u ).
err2 <-
if v == 1
then return 0
else do
let v' = ( v - u ) / ( 1 - u )
deCasteljauLeft buf deg v'
-- Propagate the changes from the temporary buffer to the parent array
forLoop 0 count \k -> do
val <- Prim.readPrimArray buf k
writeStrided coeffs k val
return $ err1 + err2
-- | Restrict a Bernstein polynomial on [0, 1] to [0, t].
--
-- Returns accumulated floating-point error.
deCasteljauLeft
:: Prim.MutablePrimArray s Double
-> Int -- ^ degree
-> Double -- ^ split point @t@
-> ST s π
deCasteljauLeft marr deg t = do
let
tInv = 1 - t
loopJ j errAcc
| j > deg = return errAcc
| otherwise = do
-- NB: backwards iteration.
let
loopI i currentErr
| i < j = return currentErr
| otherwise = do
let
val <- Prim.readPrimArray marr i
valPrev <- Prim.readPrimArray marr ( i - 1 )
let
!(# term1 , e1 #) = prodWithErr valPrev tInv
!(# term2 , e2 #) = prodWithErr val t
!(# newVal, e3 #) = addWithErr term1 term2
stepErr = e1 + e2 + e3
Prim.writePrimArray marr i newVal
loopI (i - 1) (currentErr + stepErr)
newErr <- loopI deg 0
loopJ (j + 1) (errAcc + newErr)
loopJ 1 0
-- | Restrict a Bernstein polynomial on [0, 1] to [t, 1].
--
-- Returns accumulated floating-point error.
deCasteljauRight
:: Prim.MutablePrimArray s Double
-> Int -- ^ degree
-> Double -- ^ split point @t@
-> ST s π
deCasteljauRight marr deg t = do
let
tInv = 1.0 - t
loopJ j errAcc
| j > deg = return errAcc
| otherwise = do
-- NB: forwards iteration.
let
limit = deg - j
loopI i currentErr
| i > limit = return currentErr
| otherwise = do
val <- Prim.readPrimArray marr i
valNext <- Prim.readPrimArray marr ( i + 1 )
let
!(# term1 , e1 #) = prodWithErr val tInv
!(# term2 , e2 #) = prodWithErr valNext t
!(# newVal, e3 #) = addWithErr term1 term2
stepErr = e1 + e2 + e3
Prim.writePrimArray marr i newVal
loopI (i + 1) (currentErr + stepErr)
newErr <- loopI 0 0
loopJ (j + 1) (errAcc + newErr)
loopJ 1 0
evaluateBernsteinModel
:: forall { order } ( degs :: Shape order )
. ( KnownNat order
, KnownShape degs
, Representable Double ( β order )
)
=> BernsteinModel degs -> β order -> π
evaluateBernsteinModel ( BernsteinModel coeffs startingErr ) x =
runST \ @s -> do
let
!dims = knownShape @degs
!dimsArray = fromList dims
!maxDeg = if null dims then 0 else maximum dims
!totalLen = Prim.sizeofPrimArray coeffs
-- Copy input into our a local buffer.
bufA <- Prim.newPrimArray @( ST s ) @Double totalLen
Prim.copyPrimArray bufA 0 coeffs 0 totalLen
-- A second buffer for ping/pong.
bufB <- Prim.newPrimArray @( ST s ) @Double totalLen
-- Small scratch buffer for 1D evaluations.
scratchBuf <- Prim.newPrimArray @( ST s ) @Double $ maxDeg + 1
let
-- We return the error accumulated in this step AND the buffer containing the result
processDim
:: Prim.MutablePrimArray s Double -- src
-> Prim.MutablePrimArray s Double -- dst
-> Int -- current (logical) size of src
-> ( Int, Double ) -- @(deg, t)@
-> ST s ( Int, π ) -- @(new_size, err)@
processDim src dst srcSize (deg, t) = do
let
!rowSize = deg + 1
!outputSize = srcSize `div` rowSize
stepErr <- foldMLoop 0 outputSize 0 \ i accErr -> do
let offset = i * rowSize
-- eval1D reads from 'src' and uses 'scratchBuf' internally
(val, evalErr) <- eval1D scratchBuf src offset deg t
Prim.writePrimArray dst i val
return $! accErr + evalErr
return (outputSize, stepErr)
-- Ping-pong
( finalBuf, _, _, totalRefinementErr) <-
forEachCoord @order ( bufA, bufB, totalLen, 0 ) \ ( Fin i ) ( curr, other, sz, err ) -> do
let
order = Prim.sizeofPrimArray dimsArray
-- Process the last dimension first, because it has stride 1.
--
-- Apologies for the index math, caused by 'Fin' being 1-indexed
-- while all the array indices are 0-based.
!j = order - fromIntegral i -- 0-based index from end
!deg_j = dimsArray `Prim.indexPrimArray` j
!x_j = x `index` ( Fin $ fromIntegral j + 1 )
( newSize, stepErr ) <- processDim curr other sz ( deg_j, x_j )
-- Swap buffers
return ( other, curr, newSize, err + stepErr )
finalVal <- Prim.readPrimArray finalBuf 0
return $ π finalVal finalVal + totalRefinementErr + startingErr
-- | Evaluates a 1D Bernstein polynomial at 't' using de Casteljau.
eval1D
:: Prim.MutablePrimArray s Double -- ^ scratch buffer (size >= degree + 1)
-> Prim.MutablePrimArray s Double -- ^ coefficients (only read, not written)
-> Int
-> Int
-> Double -- @t@
-> ST s ( Double, π )
eval1D buf marr off deg t = do
-- Copy the coefficients into the scratch buffer.
let
!count = deg + 1
forLoop 0 count \ k -> do
v <- Prim.readPrimArray marr ( off + k )
Prim.writePrimArray buf k v
let
!tInv = 1 - t
-- Triangle loop: Reduce degree from 'deg' down to 1
finalErr <-
foldMLoop 1 ( deg + 1 ) 0 \ r accErr -> do
-- Inner loop: compute degree r coefficients
-- We write to indices 0 .. (deg - r)
let !limit = deg - r
stepErr <- foldMLoop 0 ( limit + 1 ) 0 \ i currentErr -> do
c0 <- Prim.readPrimArray buf i
c1 <- Prim.readPrimArray buf $ i + 1
let
!(# p1 , e1 #) = prodWithErr c0 tInv
!(# p2 , e2 #) = prodWithErr c1 t
!(# val, e3 #) = addWithErr p1 p2
Prim.writePrimArray buf i val
return $ currentErr + e1 + e2 + e3
return ( accErr + stepErr )
finalVal <- Prim.readPrimArray buf 0
return (finalVal, finalErr)
-- TODO: degree lowering, degree raising
{- TODO: BΓ©zier clipping
Start with multivariate Taylor model in Bernstein basis
1. Choose a dimension/variable.
2. Perform column reduction to that dimension, taking max/min of all coefficients across other dimensions.
This gives a 1D Taylor model in Bernstein basis.
3. Use Andrew's monotone chain algorithm to compute a floor and roof,
including the Taylor error term (negative part for floor, positive part for roof).
4. Clip the floor and roof against y=0 (using interval arithmetic to account for rounding errors).
For example, for the ceiling:
- start with the first edge that starts above y=0 or crosses y=0,
- end with the last edge that ends above y=0 or crosses y=0.
This (potentially) refines the original interval in that dimension.
Then iterate across all dimensions. It may be beneficial to refine the BΓ©zier patch
after handling each dimension, as long as we shrink enough in the dimension to
warrant doing this computation (e.g. if we shrink to 99% of the orginal size in the first
dimension, probably not worth doing de Casteljau).
-}
{- FUTURE ENHANCEMENTS
- composition of Bernstein tensors/Bernstein models
-}
--------------------------------------------------------------------------------
-- Helpers
data Strided s = Strided
{ stridedArray :: !( Prim.MutablePrimArray s Double )
, stridedOffset :: !Int
, stridedStride :: !Int
}
readStrided :: Strided s -> Int -> ST s Double
readStrided ( Strided arr off str) i =
Prim.readPrimArray arr ( off + i * str )
writeStrided :: Strided s -> Int -> Double -> ST s ()
writeStrided ( Strided arr off str ) i x =
Prim.writePrimArray arr ( off + i * str ) x
{-# INLINEABLE forLoop #-}
{-# SPECIALISE INLINE forall s. forall. forLoop @( ST s ) #-}
-- | @C@-style for loop, from start to end (but not including the end).
forLoop :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
forLoop start end body = go start
where
go !i | i >= end = return ()
| otherwise = body i >> go (i + 1)
{-# INLINE foldLoop #-}
foldLoop :: Int -> Int -> a -> ( Int -> a -> a ) -> a
foldLoop start end initVal body = go start initVal
where
go !i !acc
| i >= end = acc
| otherwise = let acc' = body i acc in go (i + 1) acc'
{-# INLINEABLE foldMLoop #-}
{-# SPECIALISE INLINE forall s. forall. foldMLoop @( ST s ) #-}
foldMLoop :: Monad m => Int -> Int -> a -> ( Int -> a -> m a ) -> m a
foldMLoop start end initVal body = go start initVal
where
go !i !acc | i >= end = return acc
| otherwise = body i acc >>= go (i + 1)
zipWithCoeffs
:: ( Double -> Double -> (# Double, π #) )
-> Prim.PrimArray Double
-> Prim.PrimArray Double
-> ( π, Prim.PrimArray Double )
zipWithCoeffs op = \ c1 c2 -> runST $ do
let !len = Prim.sizeofPrimArray c1
marr <- Prim.newPrimArray len
-- Compute the hull of local floating-point errors
totalFpErr <- foldMLoop 0 len ( π (1/0) (-1/0) ) \ i accErr -> do
let
!v1 = Prim.indexPrimArray c1 i
!v2 = Prim.indexPrimArray c2 i
!(# res, err #) = op v1 v2
Prim.writePrimArray marr i res
return $! hull accErr err
resArr <- Prim.unsafeFreezePrimArray marr
return ( totalFpErr, resArr )
{-# INLINE zipWithCoeffs #-}
-- inline to expose the 'op' function