packages feed

testing-tensor-0.1.0: test/TestSuite/Test/Convolution/FFT.hs

-- | Test against a reference implementation using fast fourier transforms
--
-- We do this only for 1D tensors.
module TestSuite.Test.Convolution.FFT (tests) where

import Data.Array.CArray (CArray)
import Data.Array.IArray (IArray)
import Data.Array.IArray qualified as IA
import Data.Complex (Complex)
import Data.Ix (Ix)
import Data.Type.Nat
import Math.FFT qualified as FFT
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

import Test.Tensor qualified as Tensor
import Test.Tensor.TestValue

import TestSuite.Test.Convolution.Examples3B1B
import TestSuite.Util.TestKernel

{-------------------------------------------------------------------------------
  List of testse
-------------------------------------------------------------------------------}

tests :: TestTree
tests = testGroup "Test.Convolution.FFT" [
      testGroup "Examples" [
          testCase "weightedMovingAverage" example_weightedMovingAverage
        ]
    , testGroup "Properties" [
          testGroup "matchesModel" [
              testProperty "kernelSize3" $ prop_matchesModel @Nat3
            , testProperty "kernelSize4" $ prop_matchesModel @Nat4
            , testProperty "kernelSize5" $ prop_matchesModel @Nat5
            ]
        ]
    ]

{-------------------------------------------------------------------------------
  Examples
-------------------------------------------------------------------------------}

example_weightedMovingAverage :: Assertion
example_weightedMovingAverage =
    assertEqual "" (movingWeightedAverageResult @TestValue) $
      removePadding 2 $
        convolveFFT
          movingWeightedAverageKernel
          (movingAverageInput @TestValue)

{-------------------------------------------------------------------------------
  Properties
-------------------------------------------------------------------------------}

-- | Compare our implementation against FFT implementation
prop_matchesModel :: forall n.
     TestKernel '[n] TestValue  -- ^ Kernel
  -> NonEmptyList TestValue     -- ^ Input
  -> Property
prop_matchesModel (testKernel -> kernel) (getNonEmpty -> input) =
        convolveFFT (reverse $ Tensor.toLists kernel) input
    === ( Tensor.toLists $
            Tensor.convolve
              kernel
              (Tensor.padWith 0 (length kernel - 1) $ Tensor.dim1 input)
        )

{-------------------------------------------------------------------------------
  Convolution implementation using FFT

  FFT requires an input of even length, so if the input has odd length, we add
  an additional zero padding byte.
-------------------------------------------------------------------------------}

-- | Compute convolution using FFT
convolveFFT :: forall a. (Fractional a, Real a) => [a] -> [a] -> [a]
convolveFFT kernel input_ =
    adjustOutput needOddAdjustment $ map realToFrac $ IA.elems inv
  where
    needOddAdjustment :: Bool
    needOddAdjustment = odd (length input_ + length kernel - 1)

    input :: [a]
    input = adjustInput needOddAdjustment input_

    n, m :: Int
    n = length input
    m = length kernel

    arrInput, arrKernel :: CArray Int Double
    arrInput  = paddedArrayFromList (m + n - 1) (map realToFrac input)
    arrKernel = paddedArrayFromList (m + n - 1) (map realToFrac kernel)

    dftInput, dftKernel, dftMult :: CArray Int (Complex Double)
    dftInput  = FFT.dftRC arrInput
    dftKernel = FFT.dftRC arrKernel
    dftMult   = zipArraySameBounds (*) dftInput dftKernel

    inv :: CArray Int Double
    inv = FFT.dftCR dftMult

adjustInput :: Num a => Bool -> [a] -> [a]
adjustInput True  = (:) 0
adjustInput False = id

adjustOutput :: Bool -> [a] -> [a]
adjustOutput True  = drop 1
adjustOutput False = id

removePadding :: Int -> [a] -> [a]
removePadding n xs = take (length xs - 2 * n) (drop n xs)

{-------------------------------------------------------------------------------
  Internal auxiliary: arrays
-------------------------------------------------------------------------------}

paddedArrayFromList :: forall a e. (IArray a e, Num e)
  => Int  -- ^ Decided length of the array
  -> [e]  -- ^ List to initialize the array from
  -> a Int e
paddedArrayFromList len xs = IA.listArray (0, len - 1) (xs ++ repeat 0)

zipArraySameBounds ::
     (IArray a x, IArray a y, IArray a z, Ix i)
  => (x -> y -> z)
  -> a i x -> a i y -> a i z
zipArraySameBounds f xs ys =
    IA.listArray (IA.bounds xs) [
        f (xs IA.! i) (ys IA.! i)
      | i <- IA.indices xs
      ]