diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..c3ca58a3 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,29 @@ +name: Run Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python 3.10 + uses: actions/setup-python@v1 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Test with pytest + run: pytest + + - name: Install AF + run: apt install arrayfire + + - name: Test array_api + run: python -m pytest arrayfire/array_api diff --git a/.gitignore b/.gitignore index aa7bb5f1..047939aa 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ nosetests.xml coverage.xml *,cover +.pytest_cache # Translations *.mo @@ -56,6 +57,8 @@ docs/_build/ # PyBuilder target/ -# IDE -.idea -.vscode +# mypy +.mypy_cache + +# Virtual environment +venv diff --git a/arrayfire/array_api/README.md b/arrayfire/array_api/README.md new file mode 100644 index 00000000..df444ed6 --- /dev/null +++ b/arrayfire/array_api/README.md @@ -0,0 +1,9 @@ +# ArrayFire ArrayAPI + +Specification Documentation: [source](https://data-apis.org/array-api/latest/purpose_and_scope.html) + +Run tests + +```bash +python -m pytest arrayfire/array_api +``` diff --git a/arrayfire/array_api/__init__.py b/arrayfire/array_api/__init__.py new file mode 100644 index 00000000..675b27ad --- /dev/null +++ b/arrayfire/array_api/__init__.py @@ -0,0 +1,9 @@ +__all__ = [ + # array objects + "Array", + # dtypes + "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", + "complex64", "complex128", "bool"] + +from .array_object import Array +from .dtypes import bool, complex64, complex128, float32, float64, int16, int32, int64, uint8, uint16, uint32, uint64 diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py new file mode 100644 index 00000000..5b9ce4c7 --- /dev/null +++ b/arrayfire/array_api/array_object.py @@ -0,0 +1,940 @@ +from __future__ import annotations + +import array as py_array +import ctypes +import enum +from typing import Any, List, Optional, Tuple, Union + +# TODO replace imports from original lib with refactored ones +from arrayfire.algorithm import count +from arrayfire.array import _in_display_dims_limit + +from . import backend +from .backend import ArrayBuffer, library +from .backend.constant_array import create_constant_array +from .device import PointerSource +from .dtypes import CType +from .dtypes import bool as af_bool +from .dtypes import float32 as af_float32 +from .dtypes.helpers import Dtype, c_api_value_to_dtype, str_to_dtype + +# TODO use int | float in operators -> remove bool | complex support + + +class Array: + def __init__( + self, x: Union[None, Array, py_array.array, int, ctypes.c_void_p, List[Union[int, float]]] = None, + dtype: Union[None, Dtype, str] = None, shape: Tuple[int, ...] = (), + pointer_source: PointerSource = PointerSource.host, offset: Optional[CType] = None, + strides: Optional[Tuple[int, ...]] = None) -> None: + _no_initial_dtype = False # HACK, FIXME + + if isinstance(dtype, str): + dtype = str_to_dtype(dtype) # type: ignore[arg-type] + + if dtype is None: + _no_initial_dtype = True + dtype = af_float32 + + if x is None: + if not shape: # shape is None or empty tuple + self.arr = library.create_handle((), dtype) + return + + self.arr = library.create_handle(shape, dtype) + return + + if isinstance(x, Array): + self.arr = library.retain_array(x.arr) + return + + if isinstance(x, py_array.array): + _type_char: str = x.typecode + _array_buffer = ArrayBuffer(*x.buffer_info()) + + elif isinstance(x, list): + _array = py_array.array("f", x) # BUG [True, False] -> dtype: f32 # TODO add int and float + _type_char = _array.typecode + _array_buffer = ArrayBuffer(*_array.buffer_info()) + + elif isinstance(x, int) or isinstance(x, ctypes.c_void_p): # TODO + _array_buffer = ArrayBuffer(x if not isinstance(x, ctypes.c_void_p) else x.value) # type: ignore[arg-type] + + if not shape: + raise TypeError("Expected to receive the initial shape due to the x being a data pointer.") + + if _no_initial_dtype: + raise TypeError("Expected to receive the initial dtype due to the x being a data pointer.") + + _type_char = dtype.typecode + + else: + raise TypeError("Passed object x is an object of unsupported class.") + + if not shape: + if _array_buffer.length != 0: + shape = (_array_buffer.length, ) + else: + RuntimeError("Shape and buffer length are size invalid.") + + if not _no_initial_dtype and dtype.typecode != _type_char: + raise TypeError("Can not create array of requested type from input data type") + + if not (offset or strides): + if pointer_source == PointerSource.host: + self.arr = library.create_array(shape, dtype, _array_buffer) + return + + self.arr = library.device_array(shape, dtype, _array_buffer) + return + + self.arr = library.create_strided_array( + shape, dtype, _array_buffer, offset, strides, pointer_source) # type: ignore[arg-type] + + # Arithmetic Operators + + def __pos__(self) -> Array: + """ + Evaluates +self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the evaluated result for each element. The returned array must have the same data type + as self. + """ + return self + + def __neg__(self) -> Array: + """ + Evaluates +self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the evaluated result for each element in self. The returned array must have a data type + determined by Type Promotion Rules. + + """ + return _process_c_function(0, self, backend.sub) + + def __add__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the sum for each element of an array instance with the respective element of the array other. + + Parameters + ---------- + self : Array + Array instance (augend array). Should have a numeric data type. + other: Union[int, float, Array] + Addend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise sums. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.add) + + def __sub__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the difference for each element of an array instance with the respective element of the array other. + + The result of self_i - other_i must be the same as self_i + (-other_i) and must be governed by the same + floating-point rules as addition (see array.__add__()). + + Parameters + ---------- + self : Array + Array instance (minuend array). Should have a numeric data type. + other: Union[int, float, Array] + Subtrahend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise differences. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.sub) + + def __mul__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the product for each element of an array instance with the respective element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise products. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.mul) + + def __truediv__(self, other: Union[int, float, Array], /) -> Array: + """ + Evaluates self_i / other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array should have a floating-point data type + determined by Type Promotion Rules. + + Note + ---- + - If one or both of self and other have integer data types, the result is implementation-dependent, as type + promotion between data type “kinds” (e.g., integer versus floating-point) is unspecified. + Specification-compliant libraries may choose to raise an error or return an array containing the element-wise + results. If an array is returned, the array must have a real-valued floating-point data type. + """ + return _process_c_function(self, other, backend.div) + + def __floordiv__(self, other: Union[int, float, Array], /) -> Array: + # TODO + return NotImplemented + + def __mod__(self, other: Union[int, float, Array], /) -> Array: + """ + Evaluates self_i % other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a real-valued data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. Each element-wise result must have the same sign as the + respective element other_i. The returned array must have a real-valued floating-point data type determined + by Type Promotion Rules. + + Note + ---- + - For input arrays which promote to an integer data type, the result of division by zero is unspecified and + thus implementation-defined. + """ + return _process_c_function(self, other, backend.mod) + + def __pow__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates an implementation-dependent approximation of exponentiation by raising each element (the base) of + an array instance to the power of other_i (the exponent), where other_i is the corresponding element of the + array other. + + Parameters + ---------- + self : Array + Array instance whose elements correspond to the exponentiation base. Should have a numeric data type. + other: Union[int, float, Array] + Other array whose elements correspond to the exponentiation exponent. Must be compatible with self + (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.pow) + + # Array Operators + + def __matmul__(self, other: Array, /) -> Array: + # TODO get from blas - make vanilla version and not copy af.matmul as is + return NotImplemented + + # Bitwise Operators + + def __invert__(self) -> Array: + """ + Evaluates ~self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have an integer or boolean data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + # FIXME + out = Array() + out.arr = backend.bitnot(self.arr) + return out + + def __and__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i & other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.bitand) + + def __or__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i | other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.bitor) + + def __xor__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i ^ other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.bitxor) + + def __lshift__(self, other: Union[int, Array], /) -> Array: + """ + Evaluates self_i << other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + Each element must be greater than or equal to 0. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + return _process_c_function(self, other, backend.bitshiftl) + + def __rshift__(self, other: Union[int, Array], /) -> Array: + """ + Evaluates self_i >> other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + Each element must be greater than or equal to 0. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + return _process_c_function(self, other, backend.bitshiftr) + + # Comparison Operators + + def __lt__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i < other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.lt) + + def __le__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i <= other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.le) + + def __gt__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i > other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.gt) + + def __ge__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i >= other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.ge) + + def __eq__(self, other: Union[int, float, bool, Array], /) -> Array: # type: ignore[override] + """ + Computes the truth value of self_i == other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, bool, Array] + Other array. Must be compatible with self (see Broadcasting). May have any data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.eq) + + def __ne__(self, other: Union[int, float, bool, Array], /) -> Array: # type: ignore[override] + """ + Computes the truth value of self_i != other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, bool, Array] + Other array. Must be compatible with self (see Broadcasting). May have any data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.neq) + + # Reflected Arithmetic Operators + + def __radd__(self, other: Array, /) -> Array: + """ + Return other + self. + """ + return _process_c_function(other, self, backend.add) + + def __rsub__(self, other: Array, /) -> Array: + """ + Return other - self. + """ + return _process_c_function(other, self, backend.sub) + + def __rmul__(self, other: Array, /) -> Array: + """ + Return other * self. + """ + return _process_c_function(other, self, backend.mul) + + def __rtruediv__(self, other: Array, /) -> Array: + """ + Return other / self. + """ + return _process_c_function(other, self, backend.div) + + def __rfloordiv__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + + def __rmod__(self, other: Array, /) -> Array: + """ + Return other % self. + """ + return _process_c_function(other, self, backend.mod) + + def __rpow__(self, other: Array, /) -> Array: + """ + Return other ** self. + """ + return _process_c_function(other, self, backend.pow) + + # Reflected Array Operators + + def __rmatmul__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + + # Reflected Bitwise Operators + + def __rand__(self, other: Array, /) -> Array: + """ + Return other & self. + """ + return _process_c_function(other, self, backend.bitand) + + def __ror__(self, other: Array, /) -> Array: + """ + Return other | self. + """ + return _process_c_function(other, self, backend.bitor) + + def __rxor__(self, other: Array, /) -> Array: + """ + Return other ^ self. + """ + return _process_c_function(other, self, backend.bitxor) + + def __rlshift__(self, other: Array, /) -> Array: + """ + Return other << self. + """ + return _process_c_function(other, self, backend.bitshiftl) + + def __rrshift__(self, other: Array, /) -> Array: + """ + Return other >> self. + """ + return _process_c_function(other, self, backend.bitshiftr) + + # In-place Arithmetic Operators + + def __iadd__(self, other: Union[int, float, Array], /) -> Array: + # TODO discuss either we need to support complex and bool as other input type + """ + Return self += other. + """ + return _process_c_function(self, other, backend.add) + + def __isub__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self -= other. + """ + return _process_c_function(self, other, backend.sub) + + def __imul__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self *= other. + """ + return _process_c_function(self, other, backend.mul) + + def __itruediv__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self /= other. + """ + return _process_c_function(self, other, backend.div) + + def __ifloordiv__(self, other: Union[int, float, Array], /) -> Array: + # TODO + return NotImplemented + + def __imod__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self %= other. + """ + return _process_c_function(self, other, backend.mod) + + def __ipow__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self **= other. + """ + return _process_c_function(self, other, backend.pow) + + # In-place Array Operators + + def __imatmul__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + + # In-place Bitwise Operators + + def __iand__(self, other: Union[int, bool, Array], /) -> Array: + """ + Return self &= other. + """ + return _process_c_function(self, other, backend.bitand) + + def __ior__(self, other: Union[int, bool, Array], /) -> Array: + """ + Return self |= other. + """ + return _process_c_function(self, other, backend.bitor) + + def __ixor__(self, other: Union[int, bool, Array], /) -> Array: + """ + Return self ^= other. + """ + return _process_c_function(self, other, backend.bitxor) + + def __ilshift__(self, other: Union[int, Array], /) -> Array: + """ + Return self <<= other. + """ + return _process_c_function(self, other, backend.bitshiftl) + + def __irshift__(self, other: Union[int, Array], /) -> Array: + """ + Return self >>= other. + """ + return _process_c_function(self, other, backend.bitshiftr) + + # Methods + + def __abs__(self) -> Array: + # TODO + return NotImplemented + + def __array_namespace__(self, *, api_version: Optional[str] = None) -> Any: + # TODO + return NotImplemented + + def __bool__(self) -> bool: + # TODO consider using scalar() and is_scalar() + return NotImplemented + + def __complex__(self) -> complex: + # TODO + return NotImplemented + + def __dlpack__(self, *, stream: Union[None, int, Any] = None): # type: ignore[no-untyped-def] + # TODO implementation and expected return type -> PyCapsule + return NotImplemented + + def __dlpack_device__(self) -> Tuple[enum.Enum, int]: + # TODO + return NotImplemented + + def __float__(self) -> float: + # TODO + return NotImplemented + + def __getitem__(self, key: Union[int, slice, Tuple[Union[int, slice, ], ...], Array], /) -> Array: + """ + Returns self[key]. + + Parameters + ---------- + self : Array + Array instance. + key : Union[int, slice, Tuple[Union[int, slice, ], ...], Array] + Index key. + + Returns + ------- + out : Array + An array containing the accessed value(s). The returned array must have the same data type as self. + """ + # TODO + # API Specification - key: Union[int, slice, ellipsis, Tuple[Union[int, slice, ellipsis], ...], array]. + # consider using af.span to replace ellipsis during refactoring + out = Array() + ndims = self.ndim + + if isinstance(key, Array) and key == af_bool.c_api_value: + ndims = 1 + if count(key) == 0: + return out + + # HACK known issue + out.arr = library.index_gen(self.arr, ndims, key) # type: ignore[arg-type] + return out + + def __index__(self) -> int: + # TODO + return NotImplemented + + def __int__(self) -> int: + # TODO + return NotImplemented + + def __len__(self) -> int: + return self.shape[0] if self.shape else 0 + + def __setitem__( + self, key: Union[int, slice, Tuple[Union[int, slice, ], ...], Array], + value: Union[int, float, bool, Array], /) -> None: + # TODO + return NotImplemented # type: ignore[return-value] # FIXME + + def __str__(self) -> str: + # TODO change the look of array str. E.g., like np.array + if not _in_display_dims_limit(self.shape): + return _metadata_string(self.dtype, self.shape) + + return _metadata_string(self.dtype) + library.array_as_str(self.arr) + + def __repr__(self) -> str: + # return _metadata_string(self.dtype, self.shape) + # TODO change the look of array representation. E.g., like np.array + return library.array_as_str(self.arr) + + def to_device(self, device: Any, /, *, stream: Union[int, Any] = None) -> Array: + # TODO implementation and change device type from Any to Device + return NotImplemented + + # Attributes + + @property + def dtype(self) -> Dtype: + """ + Data type of the array elements. + + Returns + ------- + out : Dtype + Array data type. + """ + return c_api_value_to_dtype(library.get_ctype(self.arr)) + + @property + def device(self) -> Any: + # TODO + return NotImplemented + + @property + def mT(self) -> Array: + # TODO + return NotImplemented + + @property + def T(self) -> Array: + """ + Transpose of the array. + + Returns + ------- + out : Array + Two-dimensional array whose first and last dimensions (axes) are permuted in reverse order relative to + original array. The returned array must have the same data type as the original array. + + Note + ---- + - The array instance must be two-dimensional. If the array instance is not two-dimensional, an error + should be raised. + """ + if self.ndim < 2: + raise TypeError(f"Array should be at least 2-dimensional. Got {self.ndim}-dimensional array") + + # TODO add check if out.dtype == self.dtype + out = Array() + out.arr = library.transpose(self.arr, False) + return out + + @property + def size(self) -> int: + """ + Number of elements in an array. + + Returns + ------- + out : int + Number of elements in an array + + Note + ---- + - This must equal the product of the array's dimensions. + """ + # NOTE previously - elements() + return library.get_elements(self.arr) + + @property + def ndim(self) -> int: + """ + Number of array dimensions (axes). + + out : int + Number of array dimensions (axes). + """ + return library.get_numdims(self.arr) + + @property + def shape(self) -> Tuple[int, ...]: + """ + Array dimensions. + + Returns + ------- + out : tuple[int, ...] + Array dimensions. + """ + # NOTE skipping passing any None values + return library.get_dims(self.arr)[:self.ndim] + + def scalar(self) -> Union[None, int, float, bool, complex]: + """ + Return the first element of the array + """ + # TODO change the logic of this method + if self.is_empty(): + return None + + return library.get_scalar(self.arr, self.dtype) + + def is_empty(self) -> bool: + """ + Check if the array is empty i.e. it has no elements. + """ + return library.is_empty(self.arr) + + def to_list(self, row_major: bool = False) -> List[Union[None, int, float, bool, complex]]: + if self.is_empty(): + return [] + + array = _reorder(self) if row_major else self + ctypes_array = library.get_data_ptr(array.arr, array.size, array.dtype) + + if array.ndim == 1: + return list(ctypes_array) + + out = [] + for i in range(array.size): + idx = i + sub_list = [] + for j in range(array.ndim): + div = array.shape[j] + sub_list.append(idx % div) + idx //= div + out.append(ctypes_array[sub_list[::-1]]) # type: ignore[call-overload] # FIXME + return out + + def to_ctype_array(self, row_major: bool = False) -> ctypes.Array: + if self.is_empty(): + raise RuntimeError("Can not convert an empty array to ctype.") + + array = _reorder(self) if row_major else self + return library.get_data_ptr(array.arr, array.size, array.dtype) + + +def _reorder(array: Array) -> Array: + """ + Returns a reordered array to help interoperate with row major formats. + """ + if array.ndim == 1: + return array + + out = Array() + out.arr = library.reorder(array.arr, array.ndim) + return out + + +def _metadata_string(dtype: Dtype, dims: Optional[Tuple[int, ...]] = None) -> str: + return ( + "arrayfire.Array()\n" + f"Type: {dtype.typename}\n" + f"Dims: {str(dims) if dims else ''}") + + +def _process_c_function(lhs: Union[int, float, Array], rhs: Union[int, float, Array], c_function: Any) -> Array: + out = Array() + + if isinstance(lhs, Array) and isinstance(rhs, Array): + lhs_array = lhs.arr + rhs_array = rhs.arr + + elif isinstance(lhs, Array) and isinstance(rhs, (int, float)): + lhs_array = lhs.arr + rhs_array = create_constant_array(rhs, lhs.shape, lhs.dtype) + + elif isinstance(lhs, (int, float)) and isinstance(rhs, Array): + lhs_array = create_constant_array(lhs, rhs.shape, rhs.dtype) + rhs_array = rhs.arr + + else: + # FIXME in reflected operators this exception shows wrong error message + raise TypeError(f"{type(rhs)} is not supported and can not be passed to C binary function.") + + out.arr = c_function(lhs_array, rhs_array) + return out diff --git a/arrayfire/array_api/backend/__init__.py b/arrayfire/array_api/backend/__init__.py new file mode 100644 index 00000000..30253b4f --- /dev/null +++ b/arrayfire/array_api/backend/__init__.py @@ -0,0 +1,10 @@ +__all__ = [ + # Backend + "ArrayBuffer", + # Operators + "add", "sub", "mul", "div", "mod", "pow", "bitnot", "bitand", "bitor", "bitxor", "bitshiftl", "bitshiftr", "lt", + "le", "gt", "ge", "eq", "neq"] + +from .backend import ArrayBuffer +from .operators import ( + add, bitand, bitnot, bitor, bitshiftl, bitshiftr, bitxor, div, eq, ge, gt, le, lt, mod, mul, neq, pow, sub) diff --git a/arrayfire/array_api/backend/backend.py b/arrayfire/array_api/backend/backend.py new file mode 100644 index 00000000..a154555f --- /dev/null +++ b/arrayfire/array_api/backend/backend.py @@ -0,0 +1,26 @@ +import ctypes +import enum +from dataclasses import dataclass + +from ..dtypes.helpers import c_dim_t, to_str + +backend_api = ctypes.CDLL("/opt/arrayfire//lib/libafcpu.3.dylib") # Mock + + +def safe_call(c_err: int) -> None: + if c_err == _ErrorCodes.none.value: + return + + err_str = ctypes.c_char_p(0) + backend_api.af_get_last_error(ctypes.pointer(err_str), ctypes.pointer(c_dim_t(0))) + raise RuntimeError(to_str(err_str)) + + +class _ErrorCodes(enum.Enum): + none = 0 + + +@dataclass +class ArrayBuffer: + address: int + length: int = 0 diff --git a/arrayfire/array_api/backend/constant_array.py b/arrayfire/array_api/backend/constant_array.py new file mode 100644 index 00000000..85ba3d8b --- /dev/null +++ b/arrayfire/array_api/backend/constant_array.py @@ -0,0 +1,84 @@ +import ctypes +from typing import Tuple, Union + +from ..dtypes import Dtype, int64, uint64 +from ..dtypes.helpers import CShape, implicit_dtype +from .backend import backend_api, safe_call + +AFArray = ctypes.c_void_p + + +def _constant_complex(number: Union[int, float], shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__data__func__constant.htm#ga5a083b1f3cd8a72a41f151de3bdea1a2 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_constant_complex( + ctypes.pointer(out), ctypes.c_double(number.real), ctypes.c_double(number.imag), 4, + ctypes.pointer(c_shape.c_array), dtype.c_api_value) + ) + return out + + +def _constant_long(number: Union[int, float], shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__data__func__constant.htm#ga10f1c9fad1ce9e9fefd885d5a1d1fd49 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_constant_long( + ctypes.pointer(out), ctypes.c_longlong(number.real), 4, ctypes.pointer(c_shape.c_array)) + ) + return out + + +def _constant_ulong(number: Union[int, float], shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__data__func__constant.htm#ga67af670cc9314589f8134019f5e68809 + """ + # return backend_api.af_constant_ulong(arr, val, ndims, dims) + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_constant_ulong( + ctypes.pointer(out), ctypes.c_ulonglong(number.real), 4, ctypes.pointer(c_shape.c_array)) + ) + return out + + +def _constant(number: Union[int, float], shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__data__func__constant.htm#gafc51b6a98765dd24cd4139f3bde00670 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_constant( + ctypes.pointer(out), ctypes.c_double(number), 4, ctypes.pointer(c_shape.c_array), dtype.c_api_value) + ) + return out + + +def create_constant_array(number: Union[int, float], shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + dtype = implicit_dtype(number, dtype) + + # NOTE complex is not supported in Data API + # if isinstance(number, complex): + # if dtype != complex64 and dtype != complex128: + # dtype = complex64 + # return _constant_complex(number, shape, dtype) + + if dtype == int64: + return _constant_long(number, shape, dtype) + + if dtype == uint64: + return _constant_ulong(number, shape, dtype) + + return _constant(number, shape, dtype) diff --git a/arrayfire/array_api/backend/library.py b/arrayfire/array_api/backend/library.py new file mode 100644 index 00000000..9c87aa5b --- /dev/null +++ b/arrayfire/array_api/backend/library.py @@ -0,0 +1,238 @@ +import ctypes +from typing import Tuple, Union, cast + +from arrayfire.array import _get_indices # HACK replace with refactored one + +from ..device import PointerSource +from ..dtypes import CType, Dtype +from ..dtypes.helpers import CShape, c_dim_t, to_str +from .backend import ArrayBuffer, backend_api, safe_call + +AFArrayPointer = ctypes._Pointer +AFArray = ctypes.c_void_p + +# HACK, TODO replace for actual bcast_var after refactoring ~ https://github.com/arrayfire/arrayfire/pull/2871 +_bcast_var = False + +# Array management + + +def create_handle(shape: Tuple[int, ...], dtype: Dtype, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga3b8f5cf6fce69aa1574544bc2d44d7d0 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_create_handle( + ctypes.pointer(out), c_shape.original_shape, ctypes.pointer(c_shape.c_array), dtype.c_api_value) + ) + return out + + +def retain_array(arr: AFArray) -> AFArray: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga7ed45b3f881c0f6c80c5cf2af886dbab + """ + out = ctypes.c_void_p(0) + + safe_call( + backend_api.af_retain_array(ctypes.pointer(out), arr) + ) + return out + + +def create_array(shape: Tuple[int, ...], dtype: Dtype, array_buffer: ArrayBuffer, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga834be32357616d8ab735087c6f681858 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_create_array( + ctypes.pointer(out), ctypes.c_void_p(array_buffer.address), c_shape.original_shape, + ctypes.pointer(c_shape.c_array), dtype.c_api_value) + ) + return out + + +def device_array(shape: Tuple[int, ...], dtype: Dtype, array_buffer: ArrayBuffer, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#gaad4fc77f872217e7337cb53bfb623cf5 + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + safe_call( + backend_api.af_device_array( + ctypes.pointer(out), ctypes.c_void_p(array_buffer.address), c_shape.original_shape, + ctypes.pointer(c_shape.c_array), dtype.c_api_value) + ) + return out + + +def create_strided_array( + shape: Tuple[int, ...], dtype: Dtype, array_buffer: ArrayBuffer, offset: CType, strides: Tuple[int, ...], + pointer_source: PointerSource, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__internal__func__create.htm#gad31241a3437b7b8bc3cf49f85e5c4e0c + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*shape) + + if offset is None: + offset = c_dim_t(0) + + if strides is None: + strides = (1, c_shape[0], c_shape[0]*c_shape[1], c_shape[0]*c_shape[1]*c_shape[2]) + + if len(strides) < 4: + strides += (strides[-1], ) * (4 - len(strides)) + + safe_call( + backend_api.af_create_strided_array( + ctypes.pointer(out), ctypes.c_void_p(array_buffer.address), offset, c_shape.original_shape, + ctypes.pointer(c_shape.c_array), CShape(*strides).c_array, dtype.c_api_value, pointer_source.value) + ) + return out + + +def get_ctype(arr: AFArray) -> int: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga0dda6898e1c0d9a43efb56cd6a988c9b + """ + out = ctypes.c_int() + + safe_call( + backend_api.af_get_type(ctypes.pointer(out), arr) + ) + return out.value + + +def get_elements(arr: AFArray) -> int: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga6845bbe4385a60a606b88f8130252c1f + """ + out = c_dim_t(0) + + safe_call( + backend_api.af_get_elements(ctypes.pointer(out), arr) + ) + return out.value + + +def get_numdims(arr: AFArray) -> int: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#gaefa019d932ff58c2a829ce87edddd2a8 + """ + out = ctypes.c_uint(0) + + safe_call( + backend_api.af_get_numdims(ctypes.pointer(out), arr) + ) + return out.value + + +def get_dims(arr: AFArray) -> Tuple[int, ...]: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga8b90da50a532837d9763e301b2267348 + """ + d0 = c_dim_t(0) + d1 = c_dim_t(0) + d2 = c_dim_t(0) + d3 = c_dim_t(0) + + safe_call( + backend_api.af_get_dims(ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), arr) + ) + return (d0.value, d1.value, d2.value, d3.value) + + +def get_scalar(arr: AFArray, dtype: Dtype, /) -> Union[None, int, float, bool, complex]: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#gaefe2e343a74a84bd43b588218ecc09a3 + """ + out = dtype.c_type() + safe_call( + backend_api.af_get_scalar(ctypes.pointer(out), arr) + ) + return cast(Union[None, int, float, bool, complex], out.value) + + +def is_empty(arr: AFArray) -> bool: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga19c749e95314e1c77d816ad9952fb680 + """ + out = ctypes.c_bool() + safe_call( + backend_api.af_is_empty(ctypes.pointer(out), arr) + ) + return out.value + + +def get_data_ptr(arr: AFArray, size: int, dtype: Dtype, /) -> ctypes.Array: + """ + source: https://arrayfire.org/docs/group__c__api__mat.htm#ga6040dc6f0eb127402fbf62c1165f0b9d + """ + c_shape = dtype.c_type * size + ctypes_array = c_shape() + safe_call( + backend_api.af_get_data_ptr(ctypes.pointer(ctypes_array), arr) + ) + return ctypes_array + + +# Arrayfire Functions + + +def index_gen(arr: AFArray, ndims: int, key: Union[int, slice, Tuple[Union[int, slice, ], ...]], /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__index__func__index.htm#ga14a7d149dba0ed0b977335a3df9d91e6 + """ + out = ctypes.c_void_p(0) + safe_call( + backend_api.af_index_gen(ctypes.pointer(out), arr, c_dim_t(ndims), _get_indices(key).pointer) + ) + return out + + +def transpose(arr: AFArray, conjugate: bool, /) -> AFArray: + """ + https://arrayfire.org/docs/group__blas__func__transpose.htm#ga716b2b9bf190c8f8d0970aef2b57d8e7 + """ + out = ctypes.c_void_p(0) + safe_call( + backend_api.af_transpose(ctypes.pointer(out), arr, conjugate) + ) + return out + + +def reorder(arr: AFArray, ndims: int, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__manip__func__reorder.htm#ga57383f4d00a3a86eab08dddd52c3ad3d + """ + out = ctypes.c_void_p(0) + c_shape = CShape(*(tuple(reversed(range(ndims))) + tuple(range(ndims, 4)))) + safe_call( + backend_api.af_reorder(ctypes.pointer(out), arr, *c_shape) + ) + return out + + +def array_as_str(arr: AFArray) -> str: + """ + source: + - https://arrayfire.org/docs/group__print__func__tostring.htm#ga01f32ef2420b5d4592c6e4b4964b863b + - https://arrayfire.org/docs/group__device__func__free__host.htm#ga3f1149a837a7ebbe8002d5d2244e3370 + """ + arr_str = ctypes.c_char_p(0) + safe_call( + backend_api.af_array_to_string(ctypes.pointer(arr_str), "", arr, 4, True) + ) + py_str = to_str(arr_str) + safe_call( + backend_api.af_free_host(arr_str) + ) + return py_str diff --git a/arrayfire/array_api/backend/operators.py b/arrayfire/array_api/backend/operators.py new file mode 100644 index 00000000..96460f3a --- /dev/null +++ b/arrayfire/array_api/backend/operators.py @@ -0,0 +1,149 @@ +import ctypes +from typing import Callable + +from .backend import backend_api, safe_call + +AFArray = ctypes.c_void_p + +# HACK, TODO replace for actual bcast_var after refactoring ~ https://github.com/arrayfire/arrayfire/pull/2871 +_bcast_var = False + +# Arithmetic Operators + + +def add(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__add.htm#ga1dfbee755fedd680f4476803ddfe06a7 + """ + return _binary_op(backend_api.af_add, lhs, rhs) + + +def sub(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__sub.htm#ga80ff99a2e186c23614ea9f36ffc6f0a4 + """ + return _binary_op(backend_api.af_sub, lhs, rhs) + + +def mul(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__mul.htm#ga5f7588b2809ff7551d38b6a0bd583a02 + """ + return _binary_op(backend_api.af_mul, lhs, rhs) + + +def div(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__div.htm#ga21f3f97755702692ec8976934e75fde6 + """ + return _binary_op(backend_api.af_div, lhs, rhs) + + +def mod(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__mod.htm#ga01924d1b59d8886e46fabd2dc9b27e0f + """ + return _binary_op(backend_api.af_mod, lhs, rhs) + + +def pow(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__pow.htm#ga0f28be1a9c8b176a78c4a47f483e7fc6 + """ + return _binary_op(backend_api.af_pow, lhs, rhs) + + +# Bitwise Operators + +def bitnot(arr: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__bitnot.htm#gaf97e8a38aab59ed2d3a742515467d01e + """ + out = ctypes.c_void_p(0) + safe_call( + backend_api.af_bitnot(ctypes.pointer(out), arr) + ) + return out + + +def bitand(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__bitand.htm#ga45c0779ade4703708596df11cca98800 + """ + return _binary_op(backend_api.af_bitand, lhs, rhs) + + +def bitor(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__bitor.htm#ga84c99f77d1d83fd53f949b4d67b5b210 + """ + return _binary_op(backend_api.af_bitor, lhs, rhs) + + +def bitxor(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__bitxor.htm#ga8188620da6b432998e55fdd1fad22100 + """ + return _binary_op(backend_api.af_bitxor, lhs, rhs) + + +def bitshiftl(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__shiftl.htm#ga3139645aafe6f045a5cab454e9c13137 + """ + return _binary_op(backend_api.af_butshiftl, lhs, rhs) + + +def bitshiftr(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__shiftr.htm#ga4c06b9977ecf96cdfc83b5dfd1ac4895 + """ + return _binary_op(backend_api.af_bitshiftr, lhs, rhs) + + +def lt(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/arith_8h.htm#ae7aa04bf23b32bb11c4bab8bdd637103 + """ + return _binary_op(backend_api.af_lt, lhs, rhs) + + +def le(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__le.htm#gad5535ce64dbed46d0773fd494e84e922 + """ + return _binary_op(backend_api.af_le, lhs, rhs) + + +def gt(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__gt.htm#ga4e65603259515de8939899a163ebaf9e + """ + return _binary_op(backend_api.af_gt, lhs, rhs) + + +def ge(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__ge.htm#ga4513f212e0b0a22dcf4653e89c85e3d9 + """ + return _binary_op(backend_api.af_ge, lhs, rhs) + + +def eq(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__eq.htm#ga76d2da7716831616bb81effa9e163693 + """ + return _binary_op(backend_api.af_eq, lhs, rhs) + + +def neq(lhs: AFArray, rhs: AFArray, /) -> AFArray: + """ + source: https://arrayfire.org/docs/group__arith__func__neq.htm#gae4ee8bd06a410f259f1493fb811ce441 + """ + return _binary_op(backend_api.af_neq, lhs, rhs) + + +def _binary_op(c_func: Callable, lhs: AFArray, rhs: AFArray, /) -> AFArray: + out = ctypes.c_void_p(0) + safe_call(c_func(ctypes.pointer(out), lhs, rhs, _bcast_var)) + return out diff --git a/arrayfire/array_api/config.py b/arrayfire/array_api/config.py new file mode 100644 index 00000000..588cbdfd --- /dev/null +++ b/arrayfire/array_api/config.py @@ -0,0 +1,6 @@ +import platform + + +def is_arch_x86() -> bool: + machine = platform.machine() + return platform.architecture()[0][0:2] == "32" and (machine[-2:] == "86" or machine[0:3] == "arm") diff --git a/arrayfire/array_api/device.py b/arrayfire/array_api/device.py new file mode 100644 index 00000000..fde5d6a5 --- /dev/null +++ b/arrayfire/array_api/device.py @@ -0,0 +1,10 @@ +import enum + + +class PointerSource(enum.Enum): + """ + Source of the pointer. + """ + # FIXME + device = 0 + host = 1 diff --git a/arrayfire/array_api/dtypes/__init__.py b/arrayfire/array_api/dtypes/__init__.py new file mode 100644 index 00000000..e9a181aa --- /dev/null +++ b/arrayfire/array_api/dtypes/__init__.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +__all__ = [ + "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "complex64", + "complex128", "bool"] + +import ctypes +from dataclasses import dataclass +from typing import Type + +CType = Type[ctypes._SimpleCData] + + +@dataclass +class Dtype: + typecode: str + c_type: CType + typename: str + c_api_value: int # Internal use only + + +# Specification required +# int8 - Not Supported, b8? # HACK Dtype("i8", ctypes.c_char, "int8", 4) +int16 = Dtype("h", ctypes.c_short, "short int", 10) +int32 = Dtype("i", ctypes.c_int, "int", 5) +int64 = Dtype("l", ctypes.c_longlong, "long int", 8) +uint8 = Dtype("B", ctypes.c_ubyte, "unsigned_char", 7) +uint16 = Dtype("H", ctypes.c_ushort, "unsigned short int", 11) +uint32 = Dtype("I", ctypes.c_uint, "unsigned int", 6) +uint64 = Dtype("L", ctypes.c_ulonglong, "unsigned long int", 9) +float32 = Dtype("f", ctypes.c_float, "float", 0) +float64 = Dtype("d", ctypes.c_double, "double", 2) +complex64 = Dtype("F", ctypes.c_float*2, "float complext", 1) # type: ignore[arg-type] +complex128 = Dtype("D", ctypes.c_double*2, "double complext", 3) # type: ignore[arg-type] +bool = Dtype("b", ctypes.c_bool, "bool", 4) + +supported_dtypes = [ + int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128, bool +] diff --git a/arrayfire/array_api/dtypes/functions.py b/arrayfire/array_api/dtypes/functions.py new file mode 100644 index 00000000..155dc61d --- /dev/null +++ b/arrayfire/array_api/dtypes/functions.py @@ -0,0 +1,32 @@ +from typing import Tuple, Union + +from ..array_object import Array +from . import Dtype + +# TODO implement functions + + +def astype(x: Array, dtype: Dtype, /, *, copy: bool = True) -> Array: + return NotImplemented + + +def can_cast(from_: Union[Dtype, Array], to: Dtype, /) -> bool: + return NotImplemented + + +def finfo(type: Union[Dtype, Array], /): # type: ignore[no-untyped-def] + # NOTE expected return type -> finfo_object + return NotImplemented + + +def iinfo(type: Union[Dtype, Array], /): # type: ignore[no-untyped-def] + # NOTE expected return type -> iinfo_object + return NotImplemented + + +def isdtype(dtype: Dtype, kind: Union[Dtype, str, Tuple[Union[Dtype, str], ...]]) -> bool: + return NotImplemented + + +def result_type(*arrays_and_dtypes: Union[Dtype, Array]) -> Dtype: + return NotImplemented diff --git a/arrayfire/array_api/dtypes/helpers.py b/arrayfire/array_api/dtypes/helpers.py new file mode 100644 index 00000000..cf4d3064 --- /dev/null +++ b/arrayfire/array_api/dtypes/helpers.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import ctypes +from typing import Tuple, Union + +from ..config import is_arch_x86 +from . import Dtype +from . import bool as af_bool +from . import complex64, complex128, float32, float64, int64, supported_dtypes + +c_dim_t = ctypes.c_int if is_arch_x86() else ctypes.c_longlong +ShapeType = Tuple[int, ...] + + +class CShape(tuple): + def __new__(cls, *args: int) -> CShape: + cls.original_shape = len(args) + return tuple.__new__(cls, args) + + def __init__(self, x1: int = 1, x2: int = 1, x3: int = 1, x4: int = 1) -> None: + self.x1 = x1 + self.x2 = x2 + self.x3 = x3 + self.x4 = x4 + + def __repr__(self) -> str: + return f"{self.__class__.__name__}{self.x1, self.x2, self.x3, self.x4}" + + @property + def c_array(self): # type: ignore[no-untyped-def] + c_shape = c_dim_t * 4 # ctypes.c_int | ctypes.c_longlong * 4 + return c_shape(c_dim_t(self.x1), c_dim_t(self.x2), c_dim_t(self.x3), c_dim_t(self.x4)) + + +def to_str(c_str: ctypes.c_char_p) -> str: + return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] + + +def implicit_dtype(number: Union[int, float], array_dtype: Dtype) -> Dtype: + if isinstance(number, bool): + number_dtype = af_bool + if isinstance(number, int): + number_dtype = int64 + elif isinstance(number, float): + number_dtype = float64 + elif isinstance(number, complex): + number_dtype = complex128 + else: + raise TypeError(f"{type(number)} is not supported and can not be converted to af.Dtype.") + + if not (array_dtype == float32 or array_dtype == complex64): + return number_dtype + + if number_dtype == float64: + return float32 + + if number_dtype == complex128: + return complex64 + + return number_dtype + + +def c_api_value_to_dtype(value: int) -> Dtype: + for dtype in supported_dtypes: + if value == dtype.c_api_value: + return dtype + + raise TypeError("There is no supported dtype that matches passed dtype C API value.") + + +def str_to_dtype(value: int) -> Dtype: + for dtype in supported_dtypes: + if value == dtype.typecode or value == dtype.typename: + return dtype + + raise TypeError("There is no supported dtype that matches passed dtype typecode.") diff --git a/arrayfire/array_api/operators.py b/arrayfire/array_api/operators.py new file mode 100644 index 00000000..7cb4c075 --- /dev/null +++ b/arrayfire/array_api/operators.py @@ -0,0 +1,25 @@ +from typing import Callable + +from . import backend +from .array_object import Array + + +class return_copy: + # TODO merge with process_c_function in array_object + def __init__(self, func: Callable) -> None: + self.func = func + + def __call__(self, x1: Array, x2: Array) -> Array: + out = Array() + out.arr = self.func(x1.arr, x2.arr) + return out + + +@return_copy +def add(x1: Array, x2: Array, /) -> Array: + return backend.add(x1, x2) + + +@return_copy +def sub(x1: Array, x2: Array, /) -> Array: + return backend.sub(x1, x2) diff --git a/arrayfire/array_api/pytest.ini b/arrayfire/array_api/pytest.ini new file mode 100644 index 00000000..bd20cb70 --- /dev/null +++ b/arrayfire/array_api/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --isort -srx ./arrayfire/array_api +console_output_style = classic +markers = mypy diff --git a/arrayfire/array_api/tests/__init__.py b/arrayfire/array_api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/arrayfire/array_api/tests/array_object/__init__.py b/arrayfire/array_api/tests/array_object/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/arrayfire/array_api/tests/array_object/test_initialization.py b/arrayfire/array_api/tests/array_object/test_initialization.py new file mode 100644 index 00000000..4a4426cf --- /dev/null +++ b/arrayfire/array_api/tests/array_object/test_initialization.py @@ -0,0 +1,53 @@ +import array as pyarray +import math +from typing import Any, Optional, Tuple + +import pytest + +from arrayfire.array_api.array_object import Array +from arrayfire.array_api.dtypes import Dtype, float32, int16 + +# TODO add tests for array arguments: device, offset, strides +# TODO add tests for all supported dtypes on initialisation +# TODO add test generation + + +@pytest.mark.parametrize( + "array, res_dtype, res_ndim, res_size, res_shape, res_len", [ + (Array(), float32, 0, 0, (), 0), + (Array(dtype=int16), int16, 0, 0, (), 0), + (Array(dtype="short int"), int16, 0, 0, (), 0), + (Array(dtype="h"), int16, 0, 0, (), 0), + (Array(shape=(2, 3)), float32, 2, 6, (2, 3), 2), + (Array([1, 2, 3]), float32, 1, 3, (3,), 3), + (Array(pyarray.array("f", [1, 2, 3])), float32, 1, 3, (3,), 3), + (Array([1], shape=(1,), dtype=float32), float32, 1, 1, (1,), 1), # BUG + (Array(Array([1])), float32, 1, 1, (1,), 1) + ]) +def test_initialization_with_different_arguments( + array: Array, res_dtype: Dtype, res_ndim: int, res_size: int, res_shape: Tuple[int, ...], + res_len: int) -> None: + assert array.dtype == res_dtype + assert array.ndim == res_ndim + assert array.size == res_size + # NOTE math.prod from empty object returns 1, but it should work for other cases + if res_size != 0: + assert array.size == math.prod(res_shape) + assert array.shape == res_shape + assert len(array) == res_len + + +@pytest.mark.parametrize( + "array_object, dtype, shape", [ + (None, "hello world", ()), + ([[1, 2, 3], [1, 2, 3]], None, ()), + (1, None, ()), + (1, None, (1,)), + ((5, 5), None, ()), + ({1: 2, 3: 4}, None, ()) + ] +) +def test_initalization_with_unsupported_argument_types( + array_object: Any, dtype: Optional[Dtype], shape: Tuple[int, ...]) -> None: + with pytest.raises(TypeError): + Array(x=array_object, dtype=dtype, shape=shape) diff --git a/arrayfire/array_api/tests/array_object/test_methods.py b/arrayfire/array_api/tests/array_object/test_methods.py new file mode 100644 index 00000000..306ad5d5 --- /dev/null +++ b/arrayfire/array_api/tests/array_object/test_methods.py @@ -0,0 +1,31 @@ +from arrayfire.array_api.array_object import Array + +# TODO add more tests for different dtypes + + +def test_array_getitem() -> None: + array = Array([1, 2, 3, 4, 5]) + + int_item = array[2] + assert array.dtype == int_item.dtype + assert int_item.scalar() == 3 + + +def test_scalar() -> None: + array = Array([1, 2, 3]) + assert array[1].scalar() == 2 + + +def test_scalar_is_empty() -> None: + array = Array() + assert array.scalar() is None + + +def test_array_to_list() -> None: + array = Array([1, 2, 3]) + assert array.to_list() == [1, 2, 3] + + +def test_array_to_list_is_empty() -> None: + array = Array() + assert array.to_list() == [] diff --git a/arrayfire/array_api/tests/array_object/test_operators.py b/arrayfire/array_api/tests/array_object/test_operators.py new file mode 100644 index 00000000..9403f193 --- /dev/null +++ b/arrayfire/array_api/tests/array_object/test_operators.py @@ -0,0 +1,122 @@ +import operator +from typing import Any, Callable, List, Union + +import pytest + +from arrayfire.array_api.array_object import Array +from arrayfire.array_api.dtypes import bool as af_bool + +Operator = Callable[..., Any] + +arithmetic_operators = [ + [operator.add, operator.iadd], + [operator.sub, operator.isub], + [operator.mul, operator.imul], + [operator.truediv, operator.itruediv], + [operator.mod, operator.imod], + [operator.pow, operator.ipow] +] + +comparison_operators = [operator.lt, operator.le, operator.gt, operator.ge, operator.eq, operator.ne] + + +def _round(list_: List[Union[int, float]], symbols: int = 4) -> List[Union[int, float]]: + # HACK replace for e.g. abs(x1-x2) < 1e-6 ~ https://davidamos.dev/the-right-way-to-compare-floats-in-python/ + return [round(x, symbols) for x in list_] + + +def pytest_generate_tests(metafunc: Any) -> None: + if "array_origin" in metafunc.fixturenames: + metafunc.parametrize("array_origin", [ + [1, 2, 3], + # [4.2, 7.5, 5.41] # FIXME too big difference between python pow and af backend + ]) + if "arithmetic_operator" in metafunc.fixturenames: + metafunc.parametrize("arithmetic_operator", arithmetic_operators) + if "comparison_operator" in metafunc.fixturenames: + metafunc.parametrize("comparison_operator", comparison_operators) + if "operand" in metafunc.fixturenames: + metafunc.parametrize("operand", [ + 2, + 1.5, + [9, 9, 9], + ]) + if "false_operand" in metafunc.fixturenames: + metafunc.parametrize("false_operand", [ + (1, 2, 3), + ("2"), + {2.34, 523.2}, + "15" + ]) + + +def test_arithmetic_operators( + array_origin: List[Union[int, float]], arithmetic_operator: List[Operator], + operand: Union[int, float, List[Union[int, float]]]) -> None: + op = arithmetic_operator[0] + iop = arithmetic_operator[1] + + if isinstance(operand, list): + ref = [op(x, y) for x, y in zip(array_origin, operand)] + rref = [op(y, x) for x, y in zip(array_origin, operand)] + operand = Array(operand) # type: ignore[assignment] + else: + ref = [op(x, operand) for x in array_origin] + rref = [op(operand, x) for x in array_origin] + + array = Array(array_origin) + + res = op(array, operand) + ires = iop(array, operand) + rres = op(operand, array) + + assert _round(res.to_list()) == _round(ires.to_list()) == _round(ref) + assert _round(rres.to_list()) == _round(rref) + + assert res.dtype == ires.dtype == rres.dtype + assert res.ndim == ires.ndim == rres.ndim + assert res.size == ires.size == ires.size + assert res.shape == ires.shape == rres.shape + assert len(res) == len(ires) == len(rres) + + +def test_arithmetic_operators_expected_to_raise_error( + array_origin: List[Union[int, float]], arithmetic_operator: List[Operator], false_operand: Any) -> None: + array = Array(array_origin) + + with pytest.raises(TypeError): + op = arithmetic_operator[0] + op(array, false_operand) + + # BUG string type false operand never raises an error + # with pytest.raises(TypeError): + # op = arithmetic_operator[0] + # op(false_operand, array) + + with pytest.raises(TypeError): + op = arithmetic_operator[1] + op(array, false_operand) + + +def test_comparison_operators( + array_origin: List[Union[int, float]], comparison_operator: Operator, + operand: Union[int, float, List[Union[int, float]]]) -> None: + if isinstance(operand, list): + ref = [comparison_operator(x, y) for x, y in zip(array_origin, operand)] + operand = Array(operand) # type: ignore[assignment] + else: + ref = [comparison_operator(x, operand) for x in array_origin] + + array = Array(array_origin) + res = comparison_operator(array, operand) # type: ignore[arg-type] + + assert res.to_list() == ref + assert res.dtype == af_bool + + +def test_comparison_operators_expected_to_raise_error( + array_origin: List[Union[int, float]], comparison_operator: Operator, false_operand: Any) -> None: + array = Array(array_origin) + + with pytest.raises(TypeError): + comparison_operator(array, false_operand) diff --git a/arrayfire/array_api/tests/test_operators.py b/arrayfire/array_api/tests/test_operators.py new file mode 100644 index 00000000..21926a27 --- /dev/null +++ b/arrayfire/array_api/tests/test_operators.py @@ -0,0 +1,20 @@ +from typing import Any + +from arrayfire.array_api import operators +from arrayfire.array_api.array_object import Array + + +class TestArithmeticOperators: + def setup_method(self, method: Any) -> None: + self.array1 = Array([1, 2, 3]) + self.array2 = Array([4, 5, 6]) + + def test_add(self) -> None: + res = operators.add(self.array1, self.array2) + res_sum = self.array1 + self.array2 + assert res.to_list() == res_sum.to_list() == [5, 7, 9] + + def test_sub(self) -> None: + res = operators.sub(self.array1, self.array2) + res_sum = self.array1 - self.array2 + assert res.to_list() == res_sum.to_list() == [-3, -3, -3] diff --git a/arrayfire/array_api/utils.py b/arrayfire/array_api/utils.py new file mode 100644 index 00000000..195111d3 --- /dev/null +++ b/arrayfire/array_api/utils.py @@ -0,0 +1,13 @@ +from typing import Tuple, Union + +from .array_object import Array + +# TODO implement functions + + +def all(x: Array, /, *, axis: Union[None, int, Tuple[int, ...]] = None, keepdims: bool = False) -> Array: + return NotImplemented + + +def any(x: Array, /, *, axis: Union[None, int, Tuple[int, ...]] = None, keepdims: bool = False) -> Array: + return NotImplemented diff --git a/arrayfire/library.py b/arrayfire/library.py index 1b3c8b3e..df68f97d 100644 --- a/arrayfire/library.py +++ b/arrayfire/library.py @@ -506,7 +506,6 @@ def _setup(): AF_PATH = os.environ['AF_PATH'] except KeyError: AF_PATH = None - pass AF_SEARCH_PATH = AF_PATH @@ -514,7 +513,6 @@ def _setup(): CUDA_PATH = os.environ['CUDA_PATH'] except KeyError: CUDA_PATH= None - pass CUDA_FOUND = False @@ -666,7 +664,6 @@ def __init__(self): VERBOSE_LOADS = os.environ['AF_VERBOSE_LOADS'] == '1' except KeyError: VERBOSE_LOADS = False - pass for libname in libnames: try: @@ -679,7 +676,6 @@ def __init__(self): if VERBOSE_LOADS: traceback.print_exc() print('Unable to load ' + full_libname) - pass c_dim4 = c_dim_t*4 out = c_void_ptr_t(0) @@ -720,7 +716,6 @@ def __init__(self): if VERBOSE_LOADS: traceback.print_exc() print('Unable to load ' + full_libname) - pass if (self.__name is None): raise RuntimeError("Could not load any ArrayFire libraries.\n" + more_info_str) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..3b997e87 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# Build requirements +wheel~=0.38.4 + +# Development requirements +-e .[dev,test] diff --git a/setup.cfg b/setup.cfg index e5414158..84f512c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,16 +16,48 @@ classifiers = [options] packages = find: -install_requires= +install_requires = scikit-build +python_requires = + >=3.8.0 [options.packages.find] include = arrayfire exclude = examples tests +install_requires = + numpy~=1.23.4 + +[options.extras_require] +dev = + autopep8~=1.6.0 + isort~=5.10.1 + flake8~=4.0.1 + flake8-quotes~=3.2.0 + mypy~=0.942 +test = + pytest~=7.1.2 + pytest-cov~=3.0.0 + pytest-isort~=3.0.0 + pytest-flake8~=1.1.1 + pytest-mypy~=0.9.1 + +[tool:isort] +line_length = 119 +multi_line_output = 4 [flake8] +exclude = venv application-import-names = arrayfire import-order-style = pep8 +inline-quotes = double max-line-length = 119 + +[mypy] +exclude = venv +disallow_incomplete_defs = true +disallow_untyped_defs = true +ignore_missing_imports = true +show_error_codes = true +warn_return_any = true