-
Notifications
You must be signed in to change notification settings - Fork 23
Description
Hi there
Ive got a few minor implementations of some numpy / scipy functions on vectors in vDSP along with associated XCTests that I think would make good contributions to Matft.
I'm curious how to best / properly implement these into Matft, as it seems like having a single home for them makes sense, and your code seems very well organized.
I'm not entirely sure of the code structure / best place to implement the logic in a generic way that leverages Matfts existing code base.
Do you have any suggestions?
numpy.allclose() as an extension to Array (without NaN equality)
func allCloseTo(array: [Float], rtol: Float = 1e-5, atol: Float = 1e-8) -> Bool
{
precondition(self.count == array.count, "Arrays must have same size")
let absDiff = vDSP.absolute( vDSP.subtract(self, array) )
let maxAbsDiff = vDSP.maximum(absDiff)
let scaledTol = Swift.max(atol, rtol * vDSP.maximum( vDSP.absolute(self) + vDSP.absolute(array) ) )
return maxAbsDiff <= scaledTol
}
scipy.spatial.distance.cosine
func CosineDistance(_ v1: [Float], _ v2: [Float]) -> Float
{
precondition(v1.count == v2.count, "Arrays must have same size")
var dotProduct: Float = 0.0
var v1Norm: Float = 0.0
var v2Norm: Float = 0.0
let n = vDSP_Length(v1.count)
// Calculate dot product of v1 and v2
vDSP_dotpr(v1, 1, v2, 1, &dotProduct, n)
// Calculate the Euclidean norm of v1
vDSP_svesq(v1, 1, &v1Norm, n)
v1Norm = sqrt(v1Norm)
// Calculate the Euclidean norm of v2
vDSP_svesq(v2, 1, &v2Norm, n)
v2Norm = sqrt(v2Norm)
// Calculate cosine distance
let distance = 1.0 - (dotProduct / (v1Norm * v2Norm))
return distance
}
and scipy.ndimage.gaussian_filter_1d as array extensions allowing one to cache the computed gaussian kernel.
Note I only really implement the default padding of reflect so far.
static func generateGaussianKernel(sigma:Float, truncate:Float = 4.0) -> [Float]
{
let radius:Int = Int( ceil(truncate * sigma) )
let sigma2 = sigma * sigma
let x:[Float] = Array<Int>( ( -radius ... radius ) ).map { Float( $0 ) }
let x2 = vForce.pow(bases: x, exponents: [Float](repeating: 2.0, count: x.count) )
let y = vDSP.multiply(-0.5 / sigma2, x2)
let phi_x = vForce.exp(y)
return vDSP.divide(phi_x, vDSP.sum(phi_x))
}
enum PaddingMode {
case reflect
case edge
}
private func padInputArray(_ input: [Float], sigma: Float, truncate: Float, paddingMode: PaddingMode) -> [Float] {
var paddedInput = [Float]()
let windowSize = Int(2.0 * sigma * truncate + 1.0)
let padSize = Swift.max(windowSize - input.count, 0)
if padSize > 0
{
switch (paddingMode)
{
case .reflect:
var paddingStart:[Float]
var paddingEnd:[Float]
// If we pad less than our input arrays count, we select what we need from the input array
// This wont be a 'full' pad, as we wont have all items in the array
if padSize <= input.count
{
paddingStart = Array<Float>( input[ 0 ..< Int(padSize)].reversed() )
paddingEnd = Array<Float>( input[ input.count - Int(padSize) ..< input.count].reversed() )
}
// Otherwise, we repeat reflection until we accrue pad size
else
{
paddingStart = input.reversed()
paddingEnd = paddingStart
while paddingStart.count <= padSize
{
paddingStart.insert(contentsOf: paddingStart.reversed(), at: 0)
paddingEnd.append(contentsOf: paddingEnd.reversed())
paddingStart = paddingStart.reversed()
paddingEnd = paddingEnd.reversed()
}
paddingStart = Array<Float>( paddingStart.suffix( Int(sigma * truncate) ) )
paddingEnd = Array<Float>( paddingEnd.prefix( Int(sigma * truncate) ) )
}
paddedInput.append(contentsOf: paddingStart)
paddedInput.append(contentsOf: input)
paddedInput.append(contentsOf: paddingEnd)
break
case .edge:
let edge = input.first ?? 0.0
paddedInput = Array(repeating: edge, count: padSize) + input + Array(repeating: edge, count: padSize)
}
return paddedInput
}
return input
}
// Make sure your Sigma and Truncate values match above:
func gaussianFilter1D(kernel:[Float], sigma:Float, truncate:Float = 4.0, paddingMode:PaddingMode = .reflect) -> [Float]
{
let paddedInput = self.padInputArray(self, sigma:sigma, truncate:truncate, paddingMode:paddingMode)
var output = [Float](repeating: 0.0, count: self.count)
vDSP.convolve(paddedInput, withKernel: kernel, result: &output)
// Technically is this needed, our sum is always 1 ?
// vDSP.divide(output, sigma, result: &output)
// let sum = vDSP.sum(kernel)
// vDSP.multiply(sum, output, result: &output)
return output
}