Skip to content

Commit

Permalink
Merge pull request #6 from k-arindam/development
Browse files Browse the repository at this point in the history
Critical bug fixes, feature implementation and project restructure
  • Loading branch information
k-arindam authored Oct 13, 2024
2 parents b425189 + cda4099 commit 80f9cac
Show file tree
Hide file tree
Showing 129 changed files with 598 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Add the following to your `Package.swift` file:

```swift
dependencies: [
.package(url: "https://github.com/k-arindam/SwiftNP", from: "0.0.2")
.package(url: "https://github.com/k-arindam/SwiftNP", from: "0.0.3")
]
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// MLMultiArray+NDArray.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
//

import Foundation
import CoreML


public extension MLMultiArray {}

public extension NDArray {}
18 changes: 18 additions & 0 deletions Sources/SwiftNP/Core/NDArray/Conversions/UIImage+NDArray.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// UIImage+NDArray.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
//

import Foundation

#if canImport(UIKit)

import UIKit

public extension UIImage {}

public extension NDArray {}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ import Foundation

/// A class representing a multi-dimensional array (NDArray) in the SwiftNP framework.
/// This class conforms to CustomStringConvertible for custom string representation.
public final class NDArray: CustomStringConvertible {
public final class NDArray: Equatable, CustomStringConvertible {

/// Compares two NDArray instances for equality.
///
/// - Parameters:
/// - lhs: The left-hand side NDArray to compare.
/// - rhs: The right-hand side NDArray to compare.
/// - Returns: A boolean indicating whether the two NDArray instances are equal.
/// - Two NDArray instances are considered equal if they have the same shape,
/// the same data type, and their flattened data arrays are equal.
public static func == (lhs: NDArray, rhs: NDArray) -> Bool {
guard lhs.shape == rhs.shape else { return false }

guard lhs.dtype == rhs.dtype else { return false }

if let lhsData = try? lhs.flattenedData(),
let rhsData = try? rhs.flattenedData() {
return Utils.equalArray(lhsData, rhsData)
}

return false
}

/// A string representation of the NDArray, displaying its data.
public var description: String { "\(data)" }
Expand Down Expand Up @@ -79,6 +100,39 @@ public final class NDArray: CustomStringConvertible {
return NDArray(repeating: repeating, count: shape.first!, dtype: dtype) // Create NDArray with repeated values
}

/// Returns the flattened data of the NDArray as an array of numeric values.
///
/// - Throws: `SNPError.typeError` if any element is not a numeric type.
internal func flattenedData() throws(SNPError) -> [any Numeric] {
/// A recursive function to flatten the NDArray.
///
/// - Parameter array: The NDArray to be flattened.
/// - Throws: `SNPError.typeError` if any element is not a numeric type.
func flatten(_ array: NDArray) throws(SNPError) -> [any Numeric] {
var flattened = [any Numeric]() // Initialize an empty array to hold the flattened values

// Iterate over each element in the NDArray's data
for element in array.data {
// If the element is another NDArray, recursively flatten it
if let element = element as? NDArray {
flattened.append(contentsOf: try flatten(element))
}
// If the element is a numeric type, add it to the flattened array
else if let element = element as? any Numeric {
flattened.append(element)
}
// If the element is neither, throw a type error
else {
throw SNPError.typeError(.custom(key: "UnknownDType"))
}
}

return flattened // Return the flattened array
}

return try flatten(self) // Call the flatten function on the current NDArray
}

/// Returns a string representation of the NDArray including its shape, dtype, and data.
public func toString() -> String {
"SwiftNP.NDArray ----->>> shape: \(shape), dtype: \(dtype), data: \(description)"
Expand Down
175 changes: 175 additions & 0 deletions Sources/SwiftNP/Core/NDArray/Operations/Arithmetic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// Arithmetic.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
//

import Foundation

extension NDArray {
/// Adds two NDArrays element-wise
/// - Parameters:
/// - lhs: The left-hand side NDArray in the addition.
/// - rhs: The right-hand side NDArray in the addition.
/// - Returns: A new NDArray representing the result of element-wise addition.
/// - Throws: SNPError if the shapes of the NDArrays are incompatible.
public static func +(lhs: NDArray, rhs: NDArray) throws(SNPError) -> NDArray { try lhs.arithmeticOperation(rhs, ops: .addition) }

/// Subtracts two NDArrays element-wise
/// - Parameters:
/// - lhs: The left-hand side NDArray in the subtraction.
/// - rhs: The right-hand side NDArray in the subtraction.
/// - Returns: A new NDArray representing the result of element-wise subtraction.
/// - Throws: SNPError if the shapes of the NDArrays are incompatible.
public static func -(lhs: NDArray, rhs: NDArray) throws(SNPError) -> NDArray { try lhs.arithmeticOperation(rhs, ops: .subtraction) }

/// Multiplies two NDArrays element-wise
/// - Parameters:
/// - lhs: The left-hand side NDArray in the multiplication.
/// - rhs: The right-hand side NDArray in the multiplication.
/// - Returns: A new NDArray representing the result of element-wise multiplication.
/// - Throws: SNPError if the shapes of the NDArrays are incompatible.
public static func *(lhs: NDArray, rhs: NDArray) throws(SNPError) -> NDArray { try lhs.multiply(rhs) }

/// Multiplies an NDArray by a scalar
/// - Parameters:
/// - lhs: The NDArray to be multiplied.
/// - scalar: The scalar value to multiply each element by.
/// - Returns: A new NDArray with each element multiplied by the scalar.
/// - Throws: SNPError in case of computation errors.
public static func *(lhs: NDArray, scalar: Double) throws(SNPError) -> NDArray { try lhs.multiply(scalar) }

/// Multiplies an NDArray by a scalar
/// - Parameters:
/// - scalar: The scalar value to multiply each element by.
/// - rhs: The NDArray to be multiplied.
/// - Returns: A new NDArray with each element multiplied by the scalar.
/// - Throws: SNPError in case of computation errors.
public static func *(scalar: Double, rhs: NDArray) throws(SNPError) -> NDArray { try rhs.multiply(scalar) }

/// Divides two NDArrays element-wise
/// - Parameters:
/// - lhs: The left-hand side NDArray in the division.
/// - rhs: The right-hand side NDArray in the division.
/// - Returns: A new NDArray representing the result of element-wise division.
/// - Throws: SNPError if the shapes of the NDArrays are incompatible.
public static func /(lhs: NDArray, rhs: NDArray) throws(SNPError) -> NDArray { try lhs.divide(rhs) }

/// Divides an NDArray by a scalar
/// - Parameters:
/// - lhs: The NDArray to be divided.
/// - scalar: The scalar value to divide each element by.
/// - Returns: A new NDArray with each element divided by the scalar.
/// - Throws: SNPError in case of computation errors.
public static func /(lhs: NDArray, scalar: Double) throws(SNPError) -> NDArray { try lhs.divide(scalar) }

/// Performs an arithmetic operation (addition or subtraction) between the current NDArray instance and another NDArray.
///
/// - Parameters:
/// - other: The NDArray to perform the operation with.
/// - ops: The arithmetic operation to perform (addition or subtraction).
/// - Throws:
/// - `SNPError.indexError` if the shapes of the two NDArray instances do not match.
/// - `SNPError.typeError` if the data type cannot be determined or if an unknown data type is encountered.
/// - Returns: A new NDArray resulting from the specified arithmetic operation between the two NDArray instances.
private func arithmeticOperation(_ other: NDArray, ops: ArithmeticOperation) throws(SNPError) -> NDArray {
// Ensure both NDArray instances have the same shape.
guard self.shape == other.shape else { throw SNPError.indexError(.custom(key: "SameShapeRequired")) }

// Determine the appropriate data type for the result.
var dtype: DType = .float64
if self.dtype == other.dtype {
dtype = self.dtype
}

// Flatten the data from both NDArray instances for element-wise operations.
var flatArrayLHS = try self.flattenedData()
let flatArrayRHS = try other.flattenedData()

// Iterate over the flattened arrays to perform the specified operation.
for (index, element) in flatArrayLHS.enumerated() {
let lhs = element
let rhs = flatArrayRHS[index]

// Initialize a variable to hold the result of the operation.
var result: NSNumber = 0

// Perform addition or subtraction based on the specified operation.
if ops == .addition {
result = NSNumber(value: lhs.nsnumber.doubleValue + rhs.nsnumber.doubleValue)
} else if ops == .subtraction {
result = NSNumber(value: lhs.nsnumber.doubleValue - rhs.nsnumber.doubleValue)
}

// Cast the result to the appropriate data type and update the flattened array.
if let casted = dtype.cast(result) {
flatArrayLHS[index] = casted
} else {
throw SNPError.typeError(.custom(key: "UnknownDTypeOf", args: ["\(element)"]))
}
}

// Create a new NDArray from the flattened results and reshape it to the original shape.
return try NDArray(shape: self.shape, dtype: dtype, data: flatArrayLHS).reshape(to: self.shape)
}

private func multiply(_ other: NDArray) throws(SNPError) -> NDArray { other }

/// Multiplies the NDArray by a scalar value element-wise.
///
/// - Parameter scalar: The scalar value to multiply each element in the NDArray by.
/// - Returns: A new NDArray with each element multiplied by the scalar.
/// - Throws: SNPError in case of invalid types or if the operation fails.
private func multiply(_ scalar: Double) throws(SNPError) -> NDArray {

/// A recursive helper function that multiplies each element of the input (which could be a scalar or a nested array)
/// by the specified scalar.
///
/// - Parameters:
/// - input: The input which could be an NSNumber, NDArray, or an array of elements.
/// - scalar: The scalar value to multiply each element by.
/// - Returns: The result of element-wise multiplication as a new array or scalar.
/// - Throws: SNPError if the input type is unsupported or if an element cannot be processed.
func multiplyByScalar(_ input: Any, scalar: Double) throws(SNPError) -> Any {
// Check if the input is an NSNumber (a numeric type)
if let numeric = input as? NSNumber {
let doubleValue = Double(truncating: numeric) // Convert to Double
return doubleValue * scalar // Multiply by scalar
}
// Check if the input is an array (could be multi-dimensional)
else if let array = input as? [Any] {
do {
// Recursively apply multiplication to each element in the array
return try array.map { element in
if let ndarray = element as? NDArray {
// If the element is an NDArray, apply scalar multiplication recursively to its data
return try multiplyByScalar(ndarray.data, scalar: scalar)
} else {
// If it's not an NDArray, treat it as an individual element
return try multiplyByScalar(element, scalar: scalar)
}
}
} catch {
throw SNPError.typeError(.custom(key: "UnknownDType"))
}
} else {
// If the input type is unsupported, throw an error
throw SNPError.typeError(.custom(key: "UnknownDType"))
}
}

// Perform element-wise scalar multiplication on the NDArray's data
guard let result = try multiplyByScalar(self.data, scalar: scalar) as? [Any] else {
// Ensure that the result is of the expected array type, else throw an error
throw SNPError.assertionError(.custom(key: "CreateUnsuccessful"))
}

// Return a new NDArray with the multiplied data, retaining the original shape and dtype
return try NDArray(array: result)
}

private func divide(_ other: NDArray) throws(SNPError) -> NDArray { other }

private func divide(_ scalar: Double) throws(SNPError) -> NDArray { self }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// TransposeExt.swift
// Broadcasting.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// File.swift
// Convenience.swift
// SwiftNP
//
// Created by Arindam Karmakar on 11/10/24.
Expand Down Expand Up @@ -53,8 +53,9 @@ extension NDArray {
/// Initializes an NDArray from a Swift array of any type.
///
/// - Parameter array: A Swift array containing elements of any type (e.g., Int, Float, etc.).
/// - Precondition: The shape inferred from the array must match the number of elements.
/// - Throws: `SNPError.indexError` if the array is empty, `SNPError.shapeError` if the array has an inhomogeneous shape, and `SNPError.assertionError` if dtype cannot be determined.
/// - Throws: `SNPError.indexError` if the array is empty,
/// `SNPError.shapeError` if the array has an inhomogeneous shape,
/// and `SNPError.assertionError` if dtype cannot be determined.
internal convenience init(array: [Any]) throws(SNPError) {

// Ensure the array is not empty. If it is, throw an index error.
Expand Down Expand Up @@ -84,8 +85,33 @@ extension NDArray {
throw SNPError.typeError(.custom(key: "UnknownDType"))
}

// A recursive function to map the original array to the appropriate NDArray format.
func map(array: [Any]) throws(SNPError) -> Any {
// Handle one-dimensional arrays of numeric values
if let array = array as? [any Numeric] {
return NDArray(shape: [array.count], dtype: dtype, data: array)
}
// Handle two-dimensional arrays
else if let array = array as? [[Any]] {
do {
let inferredShape = Utils.inferShape(from: array) // Re-infer shape for nested arrays
return NDArray(shape: inferredShape, dtype: dtype, data: try array.map { try map(array: $0) })
} catch {
throw SNPError.typeError(.custom(key: "UnknownDType"))
}
}
// If the input type is unsupported, throw an error
else {
throw SNPError.typeError(.custom(key: "UnknownDType"))
}
}

// Initialize the NDArray with the inferred shape, detected dtype, and original array data.
self.init(shape: inferredShape, dtype: dtype, data: array)
if let ndarray = try map(array: array) as? NDArray {
self.init(shape: inferredShape, dtype: dtype, data: ndarray.data)
} else {
throw SNPError.assertionError(.custom(key: "CreateUnsuccessful"))
}
}

/// Initializes an NDArray by repeating a value a specified number of times.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// IndexingExt.swift
// Indexing.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// ReshapeExt.swift
// Reshape.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
Expand All @@ -22,7 +22,7 @@ public extension NDArray {
}

// Flatten the current data for easy reshaping
let flattenedArray = Utils.flatten(self.data)
let flattenedArray = try self.flattenedData()
let totalSize = shape.reduce(1, *) // Calculate the total size of the new shape

// Check if the total size matches the number of elements in the flattened array
Expand Down Expand Up @@ -96,6 +96,6 @@ public extension NDArray {
}

// Return the reshaped NDArray
return NDArray(shape: shape, dtype: self.dtype, data: reshapedArray)
return try NDArray(array: reshapedArray)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// StatisticalExt.swift
// Statistical.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// BroadcastingExt.swift
// Transpose.swift
// SwiftNP
//
// Created by Arindam Karmakar on 13/10/24.
Expand Down
Loading

0 comments on commit 80f9cac

Please sign in to comment.