arrayfire-0.8.0.0: test/ArrayFire/BLASSpec.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module ArrayFire.BLASSpec where
import ArrayFire hiding (not, and, abs, max, mm, tr)
import Data.Complex
import Test.Hspec
import Test.Hspec.ApproxExpect (closeList)
import Test.Hspec.QuickCheck (prop)
-- | Build a 4x4 'Double' matrix from an arbitrary (possibly short) list,
-- padding with zeros so the shape is always well-defined.
mat4 :: [Double] -> Array Double
mat4 xs = mkArray [4,4] (take 16 (xs ++ repeat 0))
-- | Build a length-4 'Double' vector, padding with zeros.
vec4 :: [Double] -> Array Double
vec4 xs = vector 4 (take 4 (xs ++ repeat 0))
-- | Plain matrix product with default (None) operands.
mm :: Array Double -> Array Double -> Array Double
mm a b = (a `matmul` b) None None
-- | Transpose (no conjugation).
tr :: Array Double -> Array Double
tr a = transpose a False
-- | Scale every element of a 4x4 matrix by a constant.
scaleMat :: Double -> Array Double -> Array Double
scaleMat c a = mkArray [4,4] (map (c *) (toList a))
spec :: Spec
spec =
describe "BLAS spec" $ do
it "Should matmul two matrices" $ do
(matrix @Double (2,2) [[2,2],[2,2]] `matmul` matrix @Double (2,2) [[2,2],[2,2]]) None None
`shouldBe` matrix @Double (2,2) [[8,8],[8,8]]
it "Should dot product two vectors" $ do
dot (vector @Double 2 (repeat 2)) (vector @Double 2 (repeat 2)) None None
`shouldBe` scalar @Double 8
it "Should produce scalar dot product between two vectors as a Complex number" $ do
dotAll (vector @Double 2 (repeat 2)) (vector @Double 2 (repeat 2)) None None
`shouldBe` 8.0 :+ 0.0
it "Should take the transpose of a matrix" $ do
transpose (matrix @Double (2,2) [[1,1],[2,2]]) False
`shouldBe` matrix @Double (2,2) [[1,2],[1,2]]
it "Should take the transpose of a matrix in place" $ do
-- transposeInPlace is an IO () that mutates the underlying C buffer.
-- All Haskell references sharing the same ForeignPtr see the result.
-- Do not use the original binding after calling this.
let m = matrix @Double (2,2) [[1,1],[2,2]]
transposeInPlace m False
m `shouldBe` matrix @Double (2,2) [[1,2],[1,2]]
it "Should perform gemm: alpha=1, A*I = A" $ do
let a = matrix @Double (2,2) [[1,2],[3,4]]
b = matrix @Double (2,2) [[1,0],[0,1]]
gemm None None 1.0 a b `shouldBe` a
it "Should perform gemm: alpha=2 scales the result" $ do
-- b col-major: col0=[3,4], col1=[5,6]
-- 2 * I * b = 2b → col0=[6,8], col1=[10,12]
let a = matrix @Double (2,2) [[1,0],[0,1]]
b = matrix @Double (2,2) [[3,4],[5,6]]
gemm None None 2.0 a b `shouldBe` matrix @Double (2,2) [[6,8],[10,12]]
it "Should perform gemm with transposed A" $ do
let a = matrix @Double (2,2) [[1,3],[2,4]]
b = matrix @Double (2,2) [[1,0],[0,1]]
gemm Trans None 1.0 a b `shouldBe` matrix @Double (2,2) [[1,2],[3,4]]
it "Should perform gemm: non-trivial A*B" $ do
-- matrix (2,2) [[c0r0,c0r1],[c1r0,c1r1]] is column-major.
-- A = [[1,3],[2,4]], B = [[5,7],[6,8]] (rows displayed by ArrayFire)
-- A*B col0 = [1*5+3*6, 2*5+4*6] = [23,34]
-- A*B col1 = [1*7+3*8, 2*7+4*8] = [31,46]
let a = matrix @Double (2,2) [[1,2],[3,4]]
b = matrix @Double (2,2) [[5,6],[7,8]]
gemm None None 1.0 a b `shouldBe` matrix @Double (2,2) [[23,34],[31,46]]
describe "algebraic properties" $ do
-- Transposition only moves data, so double-transpose is exactly the
-- identity (no floating-point rounding involved).
prop "transpose is an involution" $ \(xs :: [Double]) ->
let m = mat4 xs
in toList (transpose (transpose m False) False) == toList m
-- Multiplying by the identity matrix recovers the original.
prop "A * I = A" $ \(xs :: [Double]) ->
let a = mat4 xs
in closeList (toList ((a `matmul` identity [4,4]) None None)) (toList a)
-- (A^T B^T)^T = B A : transpose distributes over a product (reversed).
prop "(A^T B^T)^T = B A" $ \(xs :: [Double]) (ys :: [Double]) ->
let a = mat4 xs
b = mat4 ys
lhs = transpose ((transpose a False `matmul` transpose b False) None None) False
rhs = (b `matmul` a) None None
in closeList (toList lhs) (toList rhs)
-- Matrix multiplication is associative.
prop "(A*B)*C = A*(B*C)" $ \(xs :: [Double]) (ys :: [Double]) (zs :: [Double]) ->
let a = mat4 xs; b = mat4 ys; c = mat4 zs
in closeList (toList (mm (mm a b) c)) (toList (mm a (mm b c)))
-- Multiplication distributes over addition on the left.
prop "A*(B+C) = A*B + A*C" $ \(xs :: [Double]) (ys :: [Double]) (zs :: [Double]) ->
let a = mat4 xs; b = mat4 ys; c = mat4 zs
in closeList (toList (mm a (b + c))) (toList (mm a b + mm a c))
-- Multiplication distributes over addition on the right.
prop "(A+B)*C = A*C + B*C" $ \(xs :: [Double]) (ys :: [Double]) (zs :: [Double]) ->
let a = mat4 xs; b = mat4 ys; c = mat4 zs
in closeList (toList (mm (a + b) c)) (toList (mm a c + mm b c))
-- The identity is a left identity too (the existing case is right-sided).
prop "I*A = A" $ \(xs :: [Double]) ->
let a = mat4 xs
in closeList (toList (mm (identity [4,4]) a)) (toList a)
-- Transpose of a product reverses the order of the factors.
prop "(A*B)^T = B^T * A^T" $ \(xs :: [Double]) (ys :: [Double]) ->
let a = mat4 xs; b = mat4 ys
in closeList (toList (tr (mm a b))) (toList (mm (tr b) (tr a)))
-- Transpose is additive.
prop "(A+B)^T = A^T + B^T" $ \(xs :: [Double]) (ys :: [Double]) ->
let a = mat4 xs; b = mat4 ys
in closeList (toList (tr (a + b))) (toList (tr a + tr b))
-- Scalar factors pull through a product: (cA)*B = c(A*B).
prop "(cA)*B = c(A*B)" $ \(c :: Double) (xs :: [Double]) (ys :: [Double]) ->
let a = mat4 xs; b = mat4 ys
in closeList (toList (mm (scaleMat c a) b)) (toList (scaleMat c (mm a b)))
-- The zero matrix annihilates under multiplication.
prop "A*0 = 0" $ \(xs :: [Double]) ->
let a = mat4 xs
in all (== 0) (toList (mm a (mat4 [])))
-- gemm with alpha=1 and no transposition agrees with matmul.
prop "gemm None None 1 A B = A*B" $ \(xs :: [Double]) (ys :: [Double]) ->
let a = mat4 xs; b = mat4 ys
in closeList (toList (gemm None None 1.0 a b)) (toList (mm a b))
-- The dot product of real vectors is symmetric.
prop "dot x y = dot y x" $ \(xs :: [Double]) (ys :: [Double]) ->
let x = vec4 xs; y = vec4 ys
in closeList (toList (dot x y None None)) (toList (dot y x None None))