packages feed

arrayfire-0.9.0.0: src/ArrayFire/Algorithm.hs

{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns        #-}
--------------------------------------------------------------------------------
-- |
-- Module      : ArrayFire.Algorithm
-- Copyright   : David Johnson (c) 2019-2026
-- License     : BSD 3
-- Maintainer  : David Johnson <code@dmj.io>
-- Stability   : Experimental
-- Portability : GHC
--
-- Functions for aggregation, manipulation of 'Array'
--
-- @
-- module Main where
--
-- import qualified ArrayFire as A
--
-- main :: IO ()
-- main = print $ A.sum (A.vector @Double 10 [1..]) 0
-- -- ArrayFire Array
-- -- [1 1 1 1]
-- --   55.0000
-- @
--------------------------------------------------------------------------------
module ArrayFire.Algorithm where

import Data.Word (Word32)
import Foreign.C.Types (CBool)

import ArrayFire.Arith (cast)
import ArrayFire.FFI
import ArrayFire.Internal.Algorithm
import ArrayFire.Internal.Types

-- | Sum all of the elements in 'Array' along the specified dimension
--
-- >>> A.sum (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [1 1 1 1]
--    55.0000
--
-- >>> A.sum (A.matrix @Double (10,10) $ replicate 10 [1..]) 1
-- ArrayFire Array
-- [10 1 1 1]
--    10.0000
--    20.0000
--    30.0000
--    40.0000
--    50.0000
--    60.0000
--    70.0000
--    80.0000
--    90.0000
--   100.0000
--
sum
  :: AFType a
  => Array a
  -- ^ Array to sum
  -> Int
  -- ^ 0-based Dimension along which to perform sum
  -> Array a
  -- ^ Will return the sum of all values in the input array along the specified dimension
sum x (fromIntegral -> n) = (x `op1` (\p a -> af_sum p a n))

-- | Sum all of the elements in 'Array' along the specified dimension, using a default value for NaN
--
-- >>> let nan = 0/0 in A.sumNaN (A.vector @Double 10 (nan : [1..])) 0 10.0
-- ArrayFire Array
-- [1 1 1 1]
--   55.0000
sumNaN
  :: (Fractional a, AFType a)
  => Array a
  -- ^ Array to sum
  -> Int
  -- ^ Dimension along which to perform sum
  -> Double
  -- ^ Default value to use in the case of NaN
  -> Array a
  -- ^ Will return the sum of all values in the input array along the specified dimension, substituted with the default value
sumNaN n (fromIntegral -> i) d = (n `op1` (\p a -> af_sum_nan p a i d))

-- | Product all of the elements in 'Array' along the specified dimension
--
-- >>> A.product (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [1 1 1 1]
-- 3628800.0000
product
  :: AFType a
  => Array a
  -- ^ Array to product
  -> Int
  -- ^ Dimension along which to perform product
  -> Array a
  -- ^ Will return the product of all values in the input array along the specified dimension
product x (fromIntegral -> n) = (x `op1` (\p a -> af_product p a n))

-- | Product all of the elements in 'Array' along the specified dimension, using a default value for NaN
--
-- >>> let nan = 0/0 in A.productNaN (A.vector @Double 10 (nan : [1..])) 0 2.0
-- ArrayFire Array
-- [1 1 1 1]
-- 3628800.0000
productNaN
  :: (AFType a, Fractional a)
  => Array a
  -- ^ Array to product
  -> Int
  -- ^ Dimension along which to perform product
  -> Double
  -- ^ Default value to use in the case of NaN
  -> Array a
  -- ^ Will return the product of all values in the input array along the specified dimension, substituted with the default value
productNaN n (fromIntegral -> i) d = n `op1` (\p a -> af_product_nan p a i d)

-- | Take the minimum of an 'Array' along a specific dimension
--
-- >>> A.min (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [1 1 1 1]
--    1.0000
min
  :: AFType a
  => Array a
  -- ^ Array input
  -> Int
  -- ^ Dimension along which to retrieve the min element
  -> Array a
  -- ^ Will contain the minimum of all values in the input array along dim
min x (fromIntegral -> n) = x `op1` (\p a -> af_min p a n)

-- | Take the maximum of an 'Array' along a specific dimension
--
-- >>> A.max (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [1 1 1 1]
--   10.0000
max
  :: AFType a
  => Array a
  -- ^ Array input
  -> Int
  -- ^ Dimension along which to retrieve the max element
  -> Array a
  -- ^ Will contain the maximum of all values in the input array along dim
max x (fromIntegral -> n) = x `op1` (\p a -> af_max p a n)

-- | Find if all elements in an 'Array' are 'True' along a dimension
--
-- >>> A.allTrue (A.vector @CBool 10 (repeat 1)) 0
-- ArrayFire Array
-- [1 1 1 1]
--         1
allTrue
  :: AFType a
  => Array a
  -- ^ Array input
  -> Int
  -- ^ Dimension along which to see if all elements are True
  -> Array CBool
  -- ^ Will contain 1 where all elements along dim are true, 0 otherwise
allTrue x (fromIntegral -> n) =
  x `op1` (\p a -> af_all_true p a n)

-- | Find if any elements in an 'Array' are 'True' along a dimension
--
-- >>> A.anyTrue (A.vector @CBool 10 (repeat 0)) 0
-- ArrayFire Array
-- [1 1 1 1]
--         0
anyTrue
  :: AFType a
  => Array a
  -- ^ Array input
  -> Int
  -- ^ Dimension along which to see if any elements are True
  -> Array CBool
  -- ^ Will contain 1 where any element along dim is true, 0 otherwise
anyTrue x (fromIntegral -> n) =
  (x `op1` (\p a -> af_any_true p a n))

-- | Count elements in an 'Array' along a dimension
--
-- >>> A.count (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [1 1 1 1]
--        10
count
  :: forall a . AFType a
  => Array a
  -- ^ Array input
  -> Int
  -- ^ Dimension along which to count
  -> Array Int
  -- ^ Count of all elements along dimension
count x (fromIntegral -> n) =
  -- af_count produces a u32 array; cast to s64 so the data matches the
  -- declared element type (otherwise host reads via toVector/toList would
  -- read 8 bytes per element from a 4-byte-per-element buffer).
  cast (x `op1` (\p a -> af_count p a n) :: Array Word32)

-- | Sum all elements in an 'Array' along all dimensions
--
-- >>> A.sumAll (A.vector @Double 10 [1..])
-- (55.0,0.0)
sumAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
sumAll = toAFResult @a . (`infoFromArray2` af_sum_all)

-- | Sum all elements in an 'Array' along all dimensions, using a default value for NaN
--
-- >>> let nan = 0/0 in A.sumNaNAll (A.vector @Double 10 (nan : [1..])) 0.0
-- (55.0,0.0)
sumNaNAll
  :: forall a . (AFResult a, Fractional a)
  => Array a
  -- ^ Input array
  -> Double
  -- ^ NaN substitute
  -> Scalar a
  -- ^ imaginary and real part
sumNaNAll a d = toAFResult @a $ infoFromArray2 a (\p g x -> af_sum_nan_all p g x d)

-- | Product all elements in an 'Array' along all dimensions, using a default value for NaN
--
-- >>> A.productAll (A.vector @Double 10 [1..])
-- (3628800.0,0.0)
productAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
productAll = toAFResult @a . (`infoFromArray2` af_product_all)

-- | Product all elements in an 'Array' along all dimensions, using a default value for NaN
--
-- >>> A.productNaNAll (A.vector @Double 10 [1..]) 1.0
-- (3628800.0,0.0)
productNaNAll
  :: forall a . (AFResult a, Fractional a)
  => Array a
  -- ^ Input array
  -> Double
  -- ^ NaN substitute
  -> Scalar a
  -- ^ imaginary and real part
productNaNAll a d = toAFResult @a $ infoFromArray2 a (\p x y -> af_product_nan_all p x y d)

-- | Take the minimum across all elements along all dimensions in 'Array'
--
-- >>> A.minAll (A.vector @Double 10 [1..])
-- (1.0,0.0)
minAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
minAll = toAFResult @a . (`infoFromArray2` af_min_all)

-- | Take the maximum across all elements along all dimensions in 'Array'
--
-- >>> A.maxAll (A.vector @Double 10 [1..])
-- (10.0,0.0)
maxAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
maxAll = toAFResult @a . (`infoFromArray2` af_max_all)

-- | Decide if all elements along all dimensions in 'Array' are True
--
-- >>> A.allTrueAll (A.vector @CBool 10 (repeat 1))
-- (1.0, 0.0)
allTrueAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
allTrueAll = toAFResult @a . (`infoFromArray2` af_all_true_all)

-- | Decide if any elements along all dimensions in 'Array' are True
--
-- >>> A.anyTrueAll $ A.vector @CBool 10 (repeat 0)
-- (0.0,0.0)
anyTrueAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
anyTrueAll = toAFResult @a . (`infoFromArray2` af_any_true_all)

-- | Count all elements along all dimensions in 'Array'
--
-- >>> A.countAll (A.matrix @Double (100,100) (replicate 100 [1..]))
-- (10000.0,0.0)
countAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> Scalar a
  -- ^ imaginary and real part
countAll = toAFResult @a . (`infoFromArray2` af_count_all)

-- | Find the minimum element along a specified dimension in 'Array'
--
-- >>> A.imin (A.vector @Double 10 [1..]) 0
-- (ArrayFire Array
-- [1 1 1 1]
--    1.0000
-- ,ArrayFire Array
-- [1 1 1 1]
--         0
-- )
imin
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ The dimension along which the minimum value is extracted
  -> (Array a, Array Word32)
  -- ^ will contain the minimum of all values along dim, will also contain the location of minimum of all values in in along dim
imin a (fromIntegral -> n) = op2p a (\x y z -> af_imin x y z n)

-- | Find the maximum element along a specified dimension in 'Array'
--
-- >>> A.imax (A.vector @Double 10 [1..]) 0
-- (ArrayFire Array
-- [1 1 1 1]
--   10.0000
-- ,ArrayFire Array
-- [1 1 1 1]
--         9
-- )
imax
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ The dimension along which the minimum value is extracted
  -> (Array a, Array Word32)
  -- ^ will contain the maximum of all values in in along dim, will also contain the location of maximum of all values in in along dim
imax a (fromIntegral -> n) = op2p a (\x y z -> af_imax x y z n)

-- | Find the minimum element along all dimensions in 'Array'
--
-- >>> A.iminAll (A.vector @Double 10 [1..])
-- (1.0,0.0,0)
iminAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> (Scalar a, Int)
  -- ^ will contain the real part of minimum value of all elements in input in, also will contain the imaginary part of minimum value of all elements in input in, will contain the location of minimum of all values in
iminAll a = do
  let (x,y,fromIntegral -> z) = a `infoFromArray3` af_imin_all
  (toAFResult @a (x,y), z)

-- | Find the maximum element along all dimensions in 'Array'
--
-- >>> A.imaxAll (A.vector @Double 10 [1..])
-- (10.0,0.0,9)
imaxAll
  :: forall a . AFResult a
  => Array a
  -- ^ Input array
  -> (Scalar a, Int)
  -- ^ will contain the real part of maximum value of all elements in input in, also will contain the imaginary part of maximum value of all elements in input in, will contain the location of maximum of all values in
imaxAll a = do
  let (x,y,fromIntegral -> z) = a `infoFromArray3` af_imax_all
  (toAFResult @a (x,y), z)

-- | Calculate sum of 'Array' across specified dimension
--
-- >>> A.accum (A.vector @Double 10 [1..]) 0
-- ArrayFire Array
-- [10 1 1 1]
--     1.0000
--     3.0000
--     6.0000
--    10.0000
--    15.0000
--    21.0000
--    28.0000
--    36.0000
--    45.0000
--    55.0000
accum
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ Dimension along which to calculate the sum
  -> Array a
  -- ^ Contains inclusive sum
accum a (fromIntegral -> n) = a `op1` (\x y -> af_accum x y n)

-- | Scan elements of an 'Array' across a dimension, using a 'BinaryOp', specifying inclusivity.
--
-- >>> A.scan (A.vector @Double 10 [1..]) 0 Add True
-- ArrayFire Array
-- [10 1 1 1]
--     1.0000
--     3.0000
--     6.0000
--    10.0000
--    15.0000
--    21.0000
--    28.0000
--    36.0000
--    45.0000
--    55.0000
scan
  :: AFType a
  => Array a
  -- ^ The input array
  -> Int
  -- ^ The dimension along which the scan is performed
  -> BinaryOp
  -- ^ Binary operation to be used
  -> Bool
  -- ^ Should the scan be inclusive or not
  -> Array a
  -- ^ The scan of the input
scan a (fromIntegral -> d) op (fromIntegral . fromEnum -> inclusive) =
  a `op1` (\x y -> af_scan x y d (toBinaryOp op) inclusive)

-- | Scan elements of an 'Array' across a dimension, by key, using a 'BinaryOp', specifying inclusivity.
--
-- >>> A.scanByKey (A.vector @Int 7 [2..]) (A.vector @Int 10 [1..]) 1 Add True
-- ArrayFire Array
-- [10 1 1 1]
--          1
--          2
--          3
--          4
--          5
--          6
--          7
--          8
--          9
--         10
scanByKey
  :: (AFType a, AFType k)
  => Array k
  -- ^ The key array
  -> Array a
  -- ^ The input array
  -> Int
  -- ^ Dimension along which scan is performed
  -> BinaryOp
  -- ^ Type of binary operation used
  -> Bool
  -- ^ Is the scan incluside or not
  -> Array a
scanByKey a b (fromIntegral -> d) op (fromIntegral . fromEnum -> inclusive) =
  op2 a b (\x y z -> af_scan_by_key x y z d (toBinaryOp op) inclusive)

-- | Find indices where input Array is non zero
--
-- >>> A.where' (A.vector @Double 10 (repeat 0))
-- ArrayFire Array
-- [0 1 1 1]
-- <empty>
where'
  :: AFType a
  => Array a
  -- ^ Is the input array.
  -> Array Word32
  -- ^ Indices where input array is non-zero
where' = (`op1` af_where)

-- | First order numerical difference along specified dimension.
--
-- >>> A.diff1 (A.vector @Double 4 [10,35,65,95]) 0
-- ArrayFire Array
-- [3 1 1 1]
--    25.0000
--    30.0000
--    30.0000
diff1
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ Dimension along which numerical difference is performed
  -> Array a
  -- ^ Will contain first order numerical difference
diff1 a (fromIntegral -> n) = a `op1` (\p x -> af_diff1 p x n)

-- | Second order numerical difference along specified dimension.
--
-- >>> A.diff2 (A.vector @Double 5 [1.0,20,55,89,44]) 0
-- ArrayFire Array
-- [3 1 1 1]
--    16.0000
--    -1.0000
--   -79.0000
diff2
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ Dimension along which numerical difference is performed
  -> Array a
  -- ^ Will contain second order numerical difference
diff2 a (fromIntegral -> n) = a `op1` (\p x -> af_diff2 p x n)

-- | Sort an Array along a specified dimension, specifying ordering of results (ascending / descending)
--
-- >>> A.sort (A.vector @Double 4 [ 2,4,3,1 ]) 0 Asc
-- ArrayFire Array
-- [4 1 1 1]
--     1.0000
--     2.0000
--     3.0000
--     4.0000
--
-- >>> A.sort (A.vector @Double 4 [ 2,4,3,1 ]) 0 Desc
-- ArrayFire Array
-- [4 1 1 1]
--     4.0000
--     3.0000
--     2.0000
--     1.0000
sort
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ Dimension along `sort` is performed
  -> Order
  -- ^ Return results in ascending order
  -> Array a
  -- ^ Will contain sorted input
sort a (fromIntegral -> n) (fromIntegral . fromEnum -> b) =
  a `op1` (\p x -> af_sort p x n b)

-- | Sort an 'Array' along a specified dimension, specifying ordering of results (ascending / descending), returns indices of sorted results
--
-- >>> A.sortIndex (A.vector @Double 4 [3,2,1,4]) 0 Asc
-- (ArrayFire Array
-- [4 1 1 1]
--     1.0000
--     2.0000
--     3.0000
--     4.0000
-- ,ArrayFire Array
-- [4 1 1 1]
--          2
--          1
--          0
--          3
-- )
sortIndex
  :: AFType a
  => Array a
  -- ^ Input array
  -> Int
  -- ^ Dimension along `sortIndex` is performed
  -> Order
  -- ^ Return results in ascending order
  -> (Array a, Array Word32)
  -- ^ Contains the sorted, contains indices for original input
sortIndex a (fromIntegral -> n) (fromIntegral . fromEnum -> b) =
  a `op2p` (\p1 p2 p3 -> af_sort_index p1 p2 p3 n b)


-- | Data type for expressing sort order
data Order = Desc | Asc
  deriving (Enum, Show, Eq)

-- | Sort an 'Array' along a specified dimension by keys, specifying ordering of results (ascending / descending)
--
-- >>> A.sortByKey (A.vector @Double 4 [2,1,4,3]) (A.vector @Double 4 [10,9,8,7]) 0 True
-- (ArrayFire Array
-- [4 1 1 1]
--     1.0000
--     2.0000
--     3.0000
--     4.0000
-- ,ArrayFire Array
-- [4 1 1 1]
--     9.0000
--    10.0000
--     7.0000
--     8.0000
-- )
sortByKey
  :: AFType a
  => Array a
  -- ^ Keys input array
  -> Array a
  -- ^ Values input array
  -> Int
  -- ^ Dimension along which to perform the operation
  -> Order
  -- ^ Return results in ascending order
  -> (Array a, Array a)
sortByKey a1 a2 (fromIntegral -> n) (fromIntegral . fromEnum -> b) =
  op2p2 a1 a2 (\w x y z -> af_sort_by_key w x y z n b)

-- | Finds the unique values in an 'Array', specifying if sorting should occur.
--
-- >>> A.setUnique (A.vector @Double 2 [1.0,1.0]) True
-- ArrayFire Array
-- [1 1 1 1]
--    1.0000
setUnique
  :: AFType a
  => Array a
  -- ^ input array
  -> Bool
  -- ^ if true, skips the sorting steps internally
  -> Array a
  -- ^ Will contain the unique values from in
setUnique a (fromIntegral . fromEnum -> b) =
  op1 a (\x y -> af_set_unique x y b)

-- | Takes the union of two 'Array's, specifying if `setUnique` should be called first.
--
-- >>> A.setUnion (A.vector @Double 3 [3,4,5]) (A.vector @Double 3 [1,2,3]) True
-- ArrayFire Array
-- [5 1 1 1]
--     1.0000
--     2.0000
--     3.0000
--     4.0000
--     5.0000
setUnion
  :: AFType a
  => Array a
  -- ^ First input array
  -> Array a
  -- ^ Second input array
  -> Bool
  -- ^ If true, skips calling unique internally
  -> Array a
setUnion a1 a2 (fromIntegral . fromEnum -> b) =
  op2 a1 a2 (\x y z -> af_set_union x y z b)

-- | Takes the intersection of two 'Array's, specifying if `setUnique` should be called first.
--
-- >>> A.setIntersect (A.vector @Double 3 [3,4,5]) (A.vector @Double 3 [1,2,3]) True
-- ArrayFire Array
-- [1 1 1 1]
--     3.0000
setIntersect
  :: AFType a
  => Array a
  -- ^ First input array
  -> Array a
  -- ^ Second input array
  -> Bool
  -- ^ If true, skips calling unique internally
  -> Array a
  -- ^ Intersection of first and second array
setIntersect a1 a2 (fromIntegral . fromEnum -> b) =
  op2 a1 a2 (\x y z -> af_set_intersect x y z b)

-- | Sum values in 'Array' grouped by keys along a dimension.
--
-- Each contiguous run of equal keys in @keys@ produces one output element.
-- Returns @(keys_out, vals_out)@.
--
-- >>> sumByKey (vector @Int 5 [1,1,2,2,2]) (vector @Double 5 [10,20,1,2,3]) 0
-- (ArrayFire Array
-- [2 1 1 1]
--    1   2,
-- ArrayFire Array
-- [2 1 1 1]
--    30.0000   6.0000)
sumByKey
  :: AFType a
  => Array Int
  -- ^ Keys array (contiguous equal keys form a group)
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension along which to reduce
  -> (Array Int, Array a)
  -- ^ (reduced keys, reduced values)
sumByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_sum_by_key ko vo k v dim)

-- | 'sumByKey' replacing NaN values with a substitute before summing.
sumByKeyNaN
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> Double
  -- ^ Substitute for NaN values
  -> (Array Int, Array a)
  -- ^ (reduced keys, reduced values)
sumByKeyNaN keys vals (fromIntegral -> dim) nanval =
  op2p2kv keys vals (\ko vo k v -> af_sum_by_key_nan ko vo k v dim nanval)

-- | Product of values in 'Array' grouped by keys along a dimension.
productByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> (Array Int, Array a)
productByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_product_by_key ko vo k v dim)

-- | 'productByKey' replacing NaN values with a substitute before multiplying.
productByKeyNaN
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> Double
  -- ^ Substitute for NaN values
  -> (Array Int, Array a)
productByKeyNaN keys vals (fromIntegral -> dim) nanval =
  op2p2kv keys vals (\ko vo k v -> af_product_by_key_nan ko vo k v dim nanval)

-- | Minimum of values in 'Array' grouped by keys along a dimension.
minByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> (Array Int, Array a)
minByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_min_by_key ko vo k v dim)

-- | Maximum of values in 'Array' grouped by keys along a dimension.
maxByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> (Array Int, Array a)
maxByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_max_by_key ko vo k v dim)

-- | True if all values are true within each key group.
--
-- The value output is always boolean (@b8@) regardless of the input value type.
allTrueByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array (treated as boolean)
  -> Int
  -- ^ Dimension
  -> (Array Int, Array CBool)
allTrueByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_all_true_by_key ko vo k v dim)

-- | True if any value is true within each key group.
--
-- The value output is always boolean (@b8@) regardless of the input value type.
anyTrueByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array (treated as boolean)
  -> Int
  -- ^ Dimension
  -> (Array Int, Array CBool)
anyTrueByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_any_true_by_key ko vo k v dim)

-- | Count non-zero values within each key group.
--
-- The value output is always @u32@ regardless of the input value type.
countByKey
  :: AFType a
  => Array Int
  -- ^ Keys array
  -> Array a
  -- ^ Values array
  -> Int
  -- ^ Dimension
  -> (Array Int, Array Word32)
countByKey keys vals (fromIntegral -> dim) =
  op2p2kv keys vals (\ko vo k v -> af_count_by_key ko vo k v dim)