From 75c1d43f898a835e16a9b35c608460e4c8b7b34e Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 24 Jan 2023 02:35:36 +0200 Subject: [PATCH 01/29] Refactoring of basic functionality to create an empty Array --- .github/workflows/build.yaml | 23 ++ .gitignore | 9 +- arrayfire/array.py | 67 +++-- arrayfire/array_api/__init__.py | 10 + arrayfire/array_api/_array_object.py | 314 ++++++++++++++++++++++++ arrayfire/array_api/_dtypes.py | 36 +++ arrayfire/array_api/_utils.py | 27 ++ arrayfire/array_api/config.py | 6 + arrayfire/array_api/tests/__init__.py | 0 arrayfire/array_api/tests/test_array.py | 11 + arrayfire/library.py | 5 - requirements.txt | 5 + setup.cfg | 34 ++- 13 files changed, 511 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/build.yaml create mode 100644 arrayfire/array_api/__init__.py create mode 100644 arrayfire/array_api/_array_object.py create mode 100644 arrayfire/array_api/_dtypes.py create mode 100644 arrayfire/array_api/_utils.py create mode 100644 arrayfire/array_api/config.py create mode 100644 arrayfire/array_api/tests/__init__.py create mode 100644 arrayfire/array_api/tests/test_array.py create mode 100644 requirements.txt diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..0c0b1e232 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,23 @@ +name: Run Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Test with pytest + run: | + pytest diff --git a/.gitignore b/.gitignore index aa7bb5f1d..047939aae 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.py b/arrayfire/array.py index 1b71db2c7..bf71ac227 100644 --- a/arrayfire/array.py +++ b/arrayfire/array.py @@ -11,6 +11,8 @@ Array class and helper functions. """ +from .algorithm import sum, count +from .arith import cast import inspect import os from .library import * @@ -25,6 +27,7 @@ _display_dims_limit = None + def set_display_dims_limit(*dims): """ Sets the dimension limit after which array's data won't get @@ -44,6 +47,7 @@ def set_display_dims_limit(*dims): global _display_dims_limit _display_dims_limit = dims + def get_display_dims_limit(): """ Gets the dimension limit after which array's data won't get @@ -67,6 +71,7 @@ def get_display_dims_limit(): """ return _display_dims_limit + def _in_display_dims_limit(dims): if _is_running_in_py_charm: return False @@ -80,6 +85,7 @@ def _in_display_dims_limit(dims): return False return True + def _create_array(buf, numdims, idims, dtype, is_device): out_arr = c_void_ptr_t(0) c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) @@ -91,6 +97,7 @@ def _create_array(buf, numdims, idims, dtype, is_device): numdims, c_pointer(c_dims), dtype.value)) return out_arr + def _create_strided_array(buf, numdims, idims, dtype, is_device, offset, strides): out_arr = c_void_ptr_t(0) c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) @@ -112,16 +119,15 @@ def _create_strided_array(buf, numdims, idims, dtype, is_device, offset, strides location.value)) return out_arr + def _create_empty_array(numdims, idims, dtype): out_arr = c_void_ptr_t(0) - - if numdims == 0: return out_arr - c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) safe_call(backend.get().af_create_handle(c_pointer(out_arr), numdims, c_pointer(c_dims), dtype.value)) return out_arr + def constant_array(val, d0, d1=None, d2=None, d3=None, dtype=Dtype.f32): """ Internal function to create a C array. Should not be used externall. @@ -176,6 +182,7 @@ def _binary_func(lhs, rhs, c_func): return out + def _binary_funcr(lhs, rhs, c_func): out = Array() other = lhs @@ -192,9 +199,10 @@ def _binary_funcr(lhs, rhs, c_func): return out + def _ctype_to_lists(ctype_arr, dim, shape, offset=0): if (dim == 0): - return list(ctype_arr[offset : offset + shape[0]]) + return list(ctype_arr[offset: offset + shape[0]]) else: dim_len = shape[dim] res = [[]] * dim_len @@ -203,6 +211,7 @@ def _ctype_to_lists(ctype_arr, dim, shape, offset=0): offset += shape[0] return res + def _slice_to_length(key, dim): tkey = [key.start, key.stop, key.step] @@ -221,6 +230,7 @@ def _slice_to_length(key, dim): return int(((tkey[1] - tkey[0] - 1) / tkey[2]) + 1) + def _get_info(dims, buf_len): elements = 1 numdims = 0 @@ -250,6 +260,7 @@ def _get_indices(key): return inds + def _get_assign_dims(key, idims): dims = [1]*4 @@ -296,6 +307,7 @@ def _get_assign_dims(key, idims): else: raise IndexError("Invalid type while assigning to arrayfire.array") + def transpose(a, conj=False): """ Perform the transpose on an input. @@ -318,6 +330,7 @@ def transpose(a, conj=False): safe_call(backend.get().af_transpose(c_pointer(out.arr), a.arr, conj)) return out + def transpose_inplace(a, conj=False): """ Perform inplace transpose on an input. @@ -338,6 +351,7 @@ def transpose_inplace(a, conj=False): """ safe_call(backend.get().af_transpose_inplace(a.arr, conj)) + class Array(BaseArray): """ @@ -447,8 +461,8 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None super(Array, self).__init__() - buf=None - buf_len=0 + buf = None + buf_len = 0 if dtype is not None: if isinstance(dtype, str): @@ -458,7 +472,7 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None else: type_char = None - _type_char='f' + _type_char = 'f' if src is not None: @@ -469,12 +483,12 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None host = __import__("array") if isinstance(src, host.array): - buf,buf_len = src.buffer_info() + buf, buf_len = src.buffer_info() _type_char = src.typecode numdims, idims = _get_info(dims, buf_len) elif isinstance(src, list): tmp = host.array('f', src) - buf,buf_len = tmp.buffer_info() + buf, buf_len = tmp.buffer_info() _type_char = tmp.typecode numdims, idims = _get_info(dims, buf_len) elif isinstance(src, int) or isinstance(src, c_void_ptr_t): @@ -498,7 +512,7 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None raise TypeError("src is an object of unsupported class") if (type_char is not None and - type_char != _type_char): + type_char != _type_char): raise TypeError("Can not create array of requested type from input data type") if(offset is None and strides is None): self.arr = _create_array(buf, numdims, idims, to_dtype[_type_char], is_device) @@ -620,8 +634,8 @@ def strides(self): s2 = c_dim_t(0) s3 = c_dim_t(0) safe_call(backend.get().af_get_strides(c_pointer(s0), c_pointer(s1), - c_pointer(s2), c_pointer(s3), self.arr)) - strides = (s0.value,s1.value,s2.value,s3.value) + c_pointer(s2), c_pointer(s3), self.arr)) + strides = (s0.value, s1.value, s2.value, s3.value) return strides[:self.numdims()] def elements(self): @@ -680,8 +694,8 @@ def dims(self): d2 = c_dim_t(0) d3 = c_dim_t(0) safe_call(backend.get().af_get_dims(c_pointer(d0), c_pointer(d1), - c_pointer(d2), c_pointer(d3), self.arr)) - dims = (d0.value,d1.value,d2.value,d3.value) + c_pointer(d2), c_pointer(d3), self.arr)) + dims = (d0.value, d1.value, d2.value, d3.value) return dims[:self.numdims()] @property @@ -906,7 +920,7 @@ def __itruediv__(self, other): """ Perform self /= other. """ - self = _binary_func(self, other, backend.get().af_div) + self = _binary_func(self, other, backend.get().af_div) return self def __rtruediv__(self, other): @@ -925,7 +939,7 @@ def __idiv__(self, other): """ Perform other / self. """ - self = _binary_func(self, other, backend.get().af_div) + self = _binary_func(self, other, backend.get().af_div) return self def __rdiv__(self, other): @@ -944,7 +958,7 @@ def __imod__(self, other): """ Perform self %= other. """ - self = _binary_func(self, other, backend.get().af_mod) + self = _binary_func(self, other, backend.get().af_mod) return self def __rmod__(self, other): @@ -963,7 +977,7 @@ def __ipow__(self, other): """ Perform self **= other. """ - self = _binary_func(self, other, backend.get().af_pow) + self = _binary_func(self, other, backend.get().af_pow) return self def __rpow__(self, other): @@ -1106,7 +1120,7 @@ def logical_and(self, other): Return self && other. """ out = Array() - safe_call(backend.get().af_and(c_pointer(out.arr), self.arr, other.arr)) #TODO: bcast var? + safe_call(backend.get().af_and(c_pointer(out.arr), self.arr, other.arr)) # TODO: bcast var? return out def logical_or(self, other): @@ -1114,7 +1128,7 @@ def logical_or(self, other): Return self || other. """ out = Array() - safe_call(backend.get().af_or(c_pointer(out.arr), self.arr, other.arr)) #TODO: bcast var? + safe_call(backend.get().af_or(c_pointer(out.arr), self.arr, other.arr)) # TODO: bcast var? return out def __nonzero__(self): @@ -1144,12 +1158,11 @@ def __getitem__(self, key): inds = _get_indices(key) safe_call(backend.get().af_index_gen(c_pointer(out.arr), - self.arr, c_dim_t(n_dims), inds.pointer)) + self.arr, c_dim_t(n_dims), inds.pointer)) return out except RuntimeError as e: raise IndexError(str(e)) - def __setitem__(self, key, val): """ Perform self[key] = val @@ -1175,14 +1188,14 @@ def __setitem__(self, key, val): n_dims = 1 other_arr = constant_array(val, int(num), dtype=self.type()) else: - other_arr = constant_array(val, tdims[0] , tdims[1], tdims[2], tdims[3], self.type()) + other_arr = constant_array(val, tdims[0], tdims[1], tdims[2], tdims[3], self.type()) del_other = True else: other_arr = val.arr del_other = False out_arr = c_void_ptr_t(0) - inds = _get_indices(key) + inds = _get_indices(key) safe_call(backend.get().af_assign_gen(c_pointer(out_arr), self.arr, c_dim_t(n_dims), inds.pointer, @@ -1401,6 +1414,7 @@ def to_ndarray(self, output=None): safe_call(backend.get().af_get_data_ptr(c_void_ptr_t(output.ctypes.data), tmp.arr)) return output + def display(a, precision=4): """ Displays the contents of an array. @@ -1426,6 +1440,7 @@ def display(a, precision=4): safe_call(backend.get().af_print_array_gen(name.encode('utf-8'), a.arr, c_int_t(precision))) + def save_array(key, a, filename, append=False): """ Save an array to disk. @@ -1457,6 +1472,7 @@ def save_array(key, a, filename, append=False): append)) return index.value + def read_array(filename, index=None, key=None): """ Read an array from disk. @@ -1490,6 +1506,3 @@ def read_array(filename, index=None, key=None): key.encode('utf-8'))) return out - -from .algorithm import (sum, count) -from .arith import cast diff --git a/arrayfire/array_api/__init__.py b/arrayfire/array_api/__init__.py new file mode 100644 index 000000000..2eb8c5d8c --- /dev/null +++ b/arrayfire/array_api/__init__.py @@ -0,0 +1,10 @@ +__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 000000000..6930ec9f1 --- /dev/null +++ b/arrayfire/array_api/_array_object.py @@ -0,0 +1,314 @@ +from __future__ import annotations + +import array as py_array +import ctypes +import math +from dataclasses import dataclass + +from arrayfire import backend, safe_call # TODO refactoring +from arrayfire.array import _in_display_dims_limit # TODO refactoring + +from ._dtypes import Dtype, c_dim_t, float32, supported_dtypes +from ._utils import Device, PointerSource, to_str + +ShapeType = tuple[None | int, ...] + + +@dataclass +class _ArrayBuffer: + address: int | None = None + length: int = 0 + + +class Array: + # Numpy checks this attribute to know which class handles binary builtin operations, such as __add__. + # Setting to such a high value should make sure that arrayfire has priority over + # other classes, ensuring that e.g. numpy.float32(1)*arrayfire.randu(3) is handled by + # arrayfire's __radd__() instead of numpy's __add__() + __array_priority__ = 30 + + # Initialisation + _array_buffer = _ArrayBuffer() + arr = ctypes.c_void_p(0) + + def __init__( + self, x: None | Array | py_array.array | list = None, dtype: None | Dtype = None, + pointer_source: PointerSource = PointerSource.host, shape: None | tuple[int] = None, + offset: None | ctypes._SimpleCData[int] = None, strides: None | tuple[int, ...] = None) -> None: + + if isinstance(dtype, str): + dtype = _str_to_dtype(dtype) + + if dtype is None: + _no_initial_dtype = True + dtype = float32 + + if x is None: + if not shape: # shape is None or empty tuple + safe_call(backend.get().af_create_handle( + ctypes.pointer(self.arr), 0, ctypes.pointer(dim4()), dtype.c_api_value)) + return + + # NOTE: applies inplace changes for self.arr + safe_call(backend.get().af_create_handle( + ctypes.pointer(self.arr), len(shape), ctypes.pointer(dim4(*shape)), dtype.c_api_value)) + return + + if isinstance(x, Array): + safe_call(backend.get().af_retain_array(ctypes.pointer(self.arr), x.arr)) + return + + if isinstance(x, py_array.array): + _type_char = x.typecode + _array_buffer = _ArrayBuffer(*x.buffer_info()) + numdims, idims = _get_info(shape, _array_buffer.length) + + 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()) + numdims, idims = _get_info(shape, _array_buffer.length) + + 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) + numdims, idims = _get_info(shape, _array_buffer.length) + + if not math.prod(idims): + raise RuntimeError("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 _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: + safe_call(backend.get().af_create_array( + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), numdims, + ctypes.pointer(dim4(*idims)), dtype.c_api_value)) + return + + safe_call(backend.get().af_device_array( + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), numdims, + ctypes.pointer(dim4(*idims)), dtype.c_api_value)) + return + + if offset is None: # TODO + offset = c_dim_t(0) + + if strides is None: # TODO + strides = (1, idims[0], idims[0]*idims[1], idims[0]*idims[1]*idims[2]) + + if len(strides) < 4: + strides += (strides[-1], ) * (4 - len(strides)) + strides_dim4 = dim4(*strides) + + safe_call(backend.get().af_create_strided_array( + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), offset, numdims, + ctypes.pointer(dim4(*idims)), ctypes.pointer(strides_dim4), dtype.c_api_value, pointer_source.value)) + + def __str__(self) -> str: # FIXME + if not _in_display_dims_limit(self.shape): + return _metadata_string(self.dtype, self.shape) + + return _metadata_string(self.dtype) + self._as_str() + + def __repr__(self) -> str: # FIXME + return _metadata_string(self.dtype, self.shape) + + def __len__(self) -> int: + return self.shape[0] if self.shape else 0 # type: ignore[return-value] + + def __pos__(self) -> Array: + """y + Return +self + """ + return self + + def __neg__(self) -> Array: + """ + Return -self + """ + # return 0 - self + raise NotImplementedError + + def __add__(self, other: int | float | Array, /) -> Array: + """ + Return self + other. + """ + # return _binary_func(self, other, backend.get().af_add) # TODO + raise NotImplementedError + + @property + def dtype(self) -> Dtype: + out = ctypes.c_int() + safe_call(backend.get().af_get_type(ctypes.pointer(out), self.arr)) + return _c_api_value_to_dtype(out.value) + + @property + def device(self) -> Device: + raise NotImplementedError + + @property + def mT(self) -> Array: + # TODO + raise NotImplementedError + + @property + def T(self) -> Array: + # TODO + raise NotImplementedError + + @property + def size(self) -> None | int: + # NOTE previously - elements() + out = c_dim_t(0) + safe_call(backend.get().af_get_elements(ctypes.pointer(out), self.arr)) + return out.value + + @property + def ndim(self) -> int: + nd = ctypes.c_uint(0) + safe_call(backend.get().af_get_numdims(ctypes.pointer(nd), self.arr)) + return nd.value + + @property + def shape(self) -> ShapeType: + """ + Return the shape of the array as a tuple. + """ + # TODO refactor + d0 = c_dim_t(0) + d1 = c_dim_t(0) + d2 = c_dim_t(0) + d3 = c_dim_t(0) + safe_call(backend.get().af_get_dims( + ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), self.arr)) + dims = (d0.value, d1.value, d2.value, d3.value) + return dims[:self.ndim] # FIXME An array dimension must be None if and only if a dimension is unknown + + def _as_str(self) -> str: + arr_str = ctypes.c_char_p(0) + # FIXME add description to passed arguments + safe_call(backend.get().af_array_to_string(ctypes.pointer(arr_str), "", self.arr, 4, True)) + py_str = to_str(arr_str) + safe_call(backend.get().af_free_host(arr_str)) + return py_str + + # def _get_metadata_str(self, show_dims: bool = True) -> str: + # return ( + # "arrayfire.Array()\n" + # f"Type: {self.dtype.typename}\n" + # f"Dims: {str(self._dims) if show_dims else ''}") + + # @property + # def dtype(self) -> ...: + # dty = ctypes.c_int() + # safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty + +# @safe_call +# def backend() +# ... + +# @backend(safe=True) +# def af_get_type(arr) -> ...: +# dty = ctypes.c_int() +# safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty +# return dty + +# def new_dtype(): +# return af_get_type(self.arr) + + +def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: + return ( + "arrayfire.Array()\n" + f"Type: {dtype.typename}\n" + f"Dims: {str(dims) if dims else ''}") + + +def _get_info(shape: None | tuple[int], buffer_length: int) -> tuple[int, list[int]]: + # TODO refactor + if shape: + numdims = len(shape) + idims = [1]*4 + for i in range(numdims): + idims[i] = shape[i] + elif (buffer_length != 0): + idims = [buffer_length, 1, 1, 1] + numdims = 1 + else: + raise RuntimeError("Invalid size") + + return numdims, idims + + +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.") + +# TODO +# def _binary_func(lhs: int | float | Array, rhs: int | float | Array, c_func: Any) -> Array: # TODO replace Any +# out = Array() +# other = rhs + +# if is_number(rhs): +# ldims = _fill_dim4_tuple(lhs.shape) +# rty = implicit_dtype(rhs, lhs.type()) +# other = Array() +# other.arr = constant_array(rhs, ldims[0], ldims[1], ldims[2], ldims[3], rty.value) +# elif not isinstance(rhs, Array): +# raise TypeError("Invalid parameter to binary function") + +# safe_call(c_func(c_pointer(out.arr), lhs.arr, other.arr, _bcast_var.get())) + +# return out + + +def dim4(d0: int = 1, d1: int = 1, d2: int = 1, d3: int = 1): # type: ignore # FIXME + c_dim4 = c_dim_t * 4 # ctypes.c_int | ctypes.c_longlong * 4 + out = c_dim4(1, 1, 1, 1) + + for i, dim in enumerate((d0, d1, d2, d3)): + if dim is not None: + out[i] = c_dim_t(dim) + + return out + +# TODO replace candidate below +# def dim4_to_tuple(shape: ShapeType, default: int=1) -> ShapeType: +# assert(isinstance(dims, tuple)) + +# if (default is not None): +# assert(is_number(default)) + +# out = [default]*4 + +# for i, dim in enumerate(dims): +# out[i] = dim + +# return tuple(out) + +# def _fill_dim4_tuple(shape: ShapeType) -> tuple[int, ...]: +# out = tuple([1 if value is None else value for value in shape]) +# if len(out) == 4: +# return out + +# return out + (1,)*(4-len(out)) diff --git a/arrayfire/array_api/_dtypes.py b/arrayfire/array_api/_dtypes.py new file mode 100644 index 000000000..dc7ef71fd --- /dev/null +++ b/arrayfire/array_api/_dtypes.py @@ -0,0 +1,36 @@ +import ctypes +from dataclasses import dataclass +from typing import Type + +from .config import is_arch_x86 + +c_dim_t = ctypes.c_int if is_arch_x86() else ctypes.c_longlong + + +@dataclass +class Dtype: + typecode: str + c_type: Type[ctypes._SimpleCData] + 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 = [ + # int8, + int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128, bool +] diff --git a/arrayfire/array_api/_utils.py b/arrayfire/array_api/_utils.py new file mode 100644 index 000000000..e235480c2 --- /dev/null +++ b/arrayfire/array_api/_utils.py @@ -0,0 +1,27 @@ +import ctypes +import enum +import numbers +from typing import Any + + +class Device(enum.Enum): + # HACK. TODO make it real + cpu = "cpu" + gpu = "gpu" + + +class PointerSource(enum.Enum): + """ + Source of the pointer + """ + # FIXME + device = 0 + host = 1 + + +def to_str(c_str: ctypes.c_char_p) -> str: + return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] + + +def is_number(number: Any) -> bool: + return isinstance(number, numbers.Number) diff --git a/arrayfire/array_api/config.py b/arrayfire/array_api/config.py new file mode 100644 index 000000000..588cbdfd2 --- /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/tests/__init__.py b/arrayfire/array_api/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/arrayfire/array_api/tests/test_array.py b/arrayfire/array_api/tests/test_array.py new file mode 100644 index 000000000..066167ced --- /dev/null +++ b/arrayfire/array_api/tests/test_array.py @@ -0,0 +1,11 @@ +from arrayfire.array_api import Array, float32 + + +def test_empty_array() -> None: + array = Array() + + assert array.dtype == float32 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 diff --git a/arrayfire/library.py b/arrayfire/library.py index 1b3c8b3ea..df68f97d8 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 000000000..3b997e873 --- /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 e5414158b..84f512c2e 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 From b14aa9124afb0978c561ea78db9da9dc1546c5ab Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 24 Jan 2023 19:09:59 +0200 Subject: [PATCH 02/29] Replace dim4 with CShape --- arrayfire/array_api/__init__.py | 3 +- arrayfire/array_api/_array_object.py | 93 +++++++------------------ arrayfire/array_api/_dtypes.py | 37 +++++++++- arrayfire/array_api/pytest.ini | 4 ++ arrayfire/array_api/tests/test_array.py | 17 +++++ 5 files changed, 83 insertions(+), 71 deletions(-) create mode 100644 arrayfire/array_api/pytest.ini diff --git a/arrayfire/array_api/__init__.py b/arrayfire/array_api/__init__.py index 2eb8c5d8c..2de1832ab 100644 --- a/arrayfire/array_api/__init__.py +++ b/arrayfire/array_api/__init__.py @@ -6,5 +6,4 @@ "complex64", "complex128", "bool"] from ._array_object import Array -from ._dtypes import ( - bool, complex64, complex128, float32, float64, int16, int32, int64, uint8, uint16, uint32, uint64) +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 index 6930ec9f1..f271b63eb 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -2,13 +2,12 @@ import array as py_array import ctypes -import math from dataclasses import dataclass from arrayfire import backend, safe_call # TODO refactoring from arrayfire.array import _in_display_dims_limit # TODO refactoring -from ._dtypes import Dtype, c_dim_t, float32, supported_dtypes +from ._dtypes import CShape, Dtype, c_dim_t, float32, supported_dtypes from ._utils import Device, PointerSource, to_str ShapeType = tuple[None | int, ...] @@ -28,7 +27,6 @@ class Array: __array_priority__ = 30 # Initialisation - _array_buffer = _ArrayBuffer() arr = ctypes.c_void_p(0) def __init__( @@ -46,12 +44,12 @@ def __init__( if x is None: if not shape: # shape is None or empty tuple safe_call(backend.get().af_create_handle( - ctypes.pointer(self.arr), 0, ctypes.pointer(dim4()), dtype.c_api_value)) + ctypes.pointer(self.arr), 0, ctypes.pointer(CShape().c_array), dtype.c_api_value)) return # NOTE: applies inplace changes for self.arr safe_call(backend.get().af_create_handle( - ctypes.pointer(self.arr), len(shape), ctypes.pointer(dim4(*shape)), dtype.c_api_value)) + ctypes.pointer(self.arr), len(shape), ctypes.pointer(CShape(*shape).c_array), dtype.c_api_value)) return if isinstance(x, Array): @@ -61,19 +59,16 @@ def __init__( if isinstance(x, py_array.array): _type_char = x.typecode _array_buffer = _ArrayBuffer(*x.buffer_info()) - numdims, idims = _get_info(shape, _array_buffer.length) 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()) - numdims, idims = _get_info(shape, _array_buffer.length) 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) - numdims, idims = _get_info(shape, _array_buffer.length) - if not math.prod(idims): + if not shape: raise RuntimeError("Expected to receive the initial shape due to the x being a data pointer.") if _no_initial_dtype: @@ -84,34 +79,37 @@ def __init__( else: raise TypeError("Passed object x is an object of unsupported class.") + _cshape = _get_cshape(shape, _array_buffer.length) + 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: safe_call(backend.get().af_create_array( - ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), numdims, - ctypes.pointer(dim4(*idims)), dtype.c_api_value)) + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), _cshape.original_shape, + ctypes.pointer(_cshape.c_array), dtype.c_api_value)) return safe_call(backend.get().af_device_array( - ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), numdims, - ctypes.pointer(dim4(*idims)), dtype.c_api_value)) + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), _cshape.original_shape, + ctypes.pointer(_cshape.c_array), dtype.c_api_value)) return - if offset is None: # TODO + if offset is None: offset = c_dim_t(0) - if strides is None: # TODO - strides = (1, idims[0], idims[0]*idims[1], idims[0]*idims[1]*idims[2]) + if strides is None: + strides = (1, _cshape[0], _cshape[0]*_cshape[1], _cshape[0]*_cshape[1]*_cshape[2]) if len(strides) < 4: strides += (strides[-1], ) * (4 - len(strides)) - strides_dim4 = dim4(*strides) + strides_cshape = CShape(*strides).c_array safe_call(backend.get().af_create_strided_array( - ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), offset, numdims, - ctypes.pointer(dim4(*idims)), ctypes.pointer(strides_dim4), dtype.c_api_value, pointer_source.value)) + ctypes.pointer(self.arr), ctypes.c_void_p(_array_buffer.address), offset, _cshape.original_shape, + ctypes.pointer(_cshape.c_array), ctypes.pointer(strides_cshape), dtype.c_api_value, + pointer_source.value)) def __str__(self) -> str: # FIXME if not _in_display_dims_limit(self.shape): @@ -126,7 +124,7 @@ def __len__(self) -> int: return self.shape[0] if self.shape else 0 # type: ignore[return-value] def __pos__(self) -> Array: - """y + """ Return +self """ return self @@ -190,8 +188,7 @@ def shape(self) -> ShapeType: d3 = c_dim_t(0) safe_call(backend.get().af_get_dims( ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), self.arr)) - dims = (d0.value, d1.value, d2.value, d3.value) - return dims[:self.ndim] # FIXME An array dimension must be None if and only if a dimension is unknown + return (d0.value, d1.value, d2.value, d3.value)[:self.ndim] # Skip passing None values def _as_str(self) -> str: arr_str = ctypes.c_char_p(0) @@ -201,30 +198,6 @@ def _as_str(self) -> str: safe_call(backend.get().af_free_host(arr_str)) return py_str - # def _get_metadata_str(self, show_dims: bool = True) -> str: - # return ( - # "arrayfire.Array()\n" - # f"Type: {self.dtype.typename}\n" - # f"Dims: {str(self._dims) if show_dims else ''}") - - # @property - # def dtype(self) -> ...: - # dty = ctypes.c_int() - # safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty - -# @safe_call -# def backend() -# ... - -# @backend(safe=True) -# def af_get_type(arr) -> ...: -# dty = ctypes.c_int() -# safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty -# return dty - -# def new_dtype(): -# return af_get_type(self.arr) - def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: return ( @@ -233,20 +206,14 @@ def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: f"Dims: {str(dims) if dims else ''}") -def _get_info(shape: None | tuple[int], buffer_length: int) -> tuple[int, list[int]]: - # TODO refactor +def _get_cshape(shape: None | tuple[int], buffer_length: int) -> CShape: if shape: - numdims = len(shape) - idims = [1]*4 - for i in range(numdims): - idims[i] = shape[i] - elif (buffer_length != 0): - idims = [buffer_length, 1, 1, 1] - numdims = 1 - else: - raise RuntimeError("Invalid size") + return CShape(*shape) + + if buffer_length != 0: + return CShape(buffer_length) - return numdims, idims + raise RuntimeError("Shape and buffer length are size invalid.") def _c_api_value_to_dtype(value: int) -> Dtype: @@ -282,16 +249,6 @@ def _str_to_dtype(value: int) -> Dtype: # return out -def dim4(d0: int = 1, d1: int = 1, d2: int = 1, d3: int = 1): # type: ignore # FIXME - c_dim4 = c_dim_t * 4 # ctypes.c_int | ctypes.c_longlong * 4 - out = c_dim4(1, 1, 1, 1) - - for i, dim in enumerate((d0, d1, d2, d3)): - if dim is not None: - out[i] = c_dim_t(dim) - - return out - # TODO replace candidate below # def dim4_to_tuple(shape: ShapeType, default: int=1) -> ShapeType: # assert(isinstance(dims, tuple)) diff --git a/arrayfire/array_api/_dtypes.py b/arrayfire/array_api/_dtypes.py index dc7ef71fd..7f099730f 100644 --- a/arrayfire/array_api/_dtypes.py +++ b/arrayfire/array_api/_dtypes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ctypes from dataclasses import dataclass from typing import Type @@ -31,6 +33,39 @@ class Dtype: bool = Dtype("b", ctypes.c_bool, "bool", 4) supported_dtypes = [ - # int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128, bool ] + + +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)) + + +# @safe_call +# def backend() +# ... + +# @backend(safe=True) +# def af_get_type(arr) -> ...: +# dty = ctypes.c_int() +# safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty +# return dty + +# def new_dtype(): +# return af_get_type(self.arr) diff --git a/arrayfire/array_api/pytest.ini b/arrayfire/array_api/pytest.ini new file mode 100644 index 000000000..1337bcae1 --- /dev/null +++ b/arrayfire/array_api/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --mypy --isort ./arrayfire/array_api +console_output_style = classic +markers = mypy diff --git a/arrayfire/array_api/tests/test_array.py b/arrayfire/array_api/tests/test_array.py index 066167ced..0cf3c9836 100644 --- a/arrayfire/array_api/tests/test_array.py +++ b/arrayfire/array_api/tests/test_array.py @@ -1,3 +1,5 @@ +import pytest + from arrayfire.array_api import Array, float32 @@ -9,3 +11,18 @@ def test_empty_array() -> None: assert array.size == 0 assert array.shape == () assert len(array) == 0 + + +def test_array_from_1d_list() -> None: + array = Array([1, 2, 3]) + + assert array.dtype == float32 + assert array.ndim == 1 + assert array.size == 3 + assert array.shape == (3,) + assert len(array) == 3 + + +def test_array_from_2d_list() -> None: + with pytest.raises(TypeError): + Array([[1, 2, 3], [1, 2, 3]]) From eadbe9b5685f8621a0fd8ecc8bb0f4b899fb8cbb Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 24 Jan 2023 19:55:53 +0200 Subject: [PATCH 03/29] Add tests. Minor fixes. Update CI --- .github/workflows/build.yaml | 11 ++- arrayfire/array_api/_array_object.py | 13 +-- arrayfire/array_api/pytest.ini | 2 +- arrayfire/array_api/tests/test_array.py | 28 ------ .../array_api/tests/test_array_object.py | 95 +++++++++++++++++++ 5 files changed, 110 insertions(+), 39 deletions(-) delete mode 100644 arrayfire/array_api/tests/test_array.py create mode 100644 arrayfire/array_api/tests/test_array_object.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0c0b1e232..e86d3fc90 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,15 +9,18 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v1 with: - python-version: 3.9 + 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 + run: pytest + + - name: Test array_api + run: python -m pytest arrayfire/array_api diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index f271b63eb..9e9c31d89 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -10,7 +10,7 @@ from ._dtypes import CShape, Dtype, c_dim_t, float32, supported_dtypes from ._utils import Device, PointerSource, to_str -ShapeType = tuple[None | int, ...] +ShapeType = tuple[int, ...] @dataclass @@ -30,9 +30,10 @@ class Array: arr = ctypes.c_void_p(0) def __init__( - self, x: None | Array | py_array.array | list = None, dtype: None | Dtype = None, - pointer_source: PointerSource = PointerSource.host, shape: None | tuple[int] = None, - offset: None | ctypes._SimpleCData[int] = None, strides: None | tuple[int, ...] = None) -> None: + self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, dtype: None | Dtype = None, + pointer_source: PointerSource = PointerSource.host, shape: None | ShapeType = None, + offset: None | ctypes._SimpleCData[int] = None, strides: None | ShapeType = None) -> None: + _no_initial_dtype = False # HACK, FIXME if isinstance(dtype, str): dtype = _str_to_dtype(dtype) @@ -69,7 +70,7 @@ def __init__( _array_buffer = _ArrayBuffer(x if not isinstance(x, ctypes.c_void_p) else x.value) if not shape: - raise RuntimeError("Expected to receive the initial shape due to the x being a data pointer.") + 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.") @@ -206,7 +207,7 @@ def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: f"Dims: {str(dims) if dims else ''}") -def _get_cshape(shape: None | tuple[int], buffer_length: int) -> CShape: +def _get_cshape(shape: None | ShapeType, buffer_length: int) -> CShape: if shape: return CShape(*shape) diff --git a/arrayfire/array_api/pytest.ini b/arrayfire/array_api/pytest.ini index 1337bcae1..43feed854 100644 --- a/arrayfire/array_api/pytest.ini +++ b/arrayfire/array_api/pytest.ini @@ -1,4 +1,4 @@ [pytest] -addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --mypy --isort ./arrayfire/array_api +addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --isort ./arrayfire/array_api console_output_style = classic markers = mypy diff --git a/arrayfire/array_api/tests/test_array.py b/arrayfire/array_api/tests/test_array.py deleted file mode 100644 index 0cf3c9836..000000000 --- a/arrayfire/array_api/tests/test_array.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest - -from arrayfire.array_api import Array, float32 - - -def test_empty_array() -> None: - array = Array() - - assert array.dtype == float32 - assert array.ndim == 0 - assert array.size == 0 - assert array.shape == () - assert len(array) == 0 - - -def test_array_from_1d_list() -> None: - array = Array([1, 2, 3]) - - assert array.dtype == float32 - assert array.ndim == 1 - assert array.size == 3 - assert array.shape == (3,) - assert len(array) == 3 - - -def test_array_from_2d_list() -> None: - with pytest.raises(TypeError): - Array([[1, 2, 3], [1, 2, 3]]) diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py new file mode 100644 index 000000000..e52f83556 --- /dev/null +++ b/arrayfire/array_api/tests/test_array_object.py @@ -0,0 +1,95 @@ +import pytest + +from arrayfire.array_api import Array, float32, int16 +from arrayfire.array_api._dtypes import supported_dtypes + + +def test_empty_array() -> None: + array = Array() + + assert array.dtype == float32 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 + + +def test_empty_array_with_nonempty_dtype() -> None: + array = Array(dtype=int16) + + assert array.dtype == int16 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 + + +def test_empty_array_with_nonempty_shape() -> None: + array = Array(shape=(2, 3)) + + assert array.dtype == float32 + assert array.ndim == 2 + assert array.size == 6 + assert array.shape == (2, 3) + assert len(array) == 2 + + +def test_array_from_1d_list() -> None: + array = Array([1, 2, 3]) + + assert array.dtype == float32 + assert array.ndim == 1 + assert array.size == 3 + assert array.shape == (3,) + assert len(array) == 3 + + +def test_array_from_2d_list() -> None: + with pytest.raises(TypeError): + Array([[1, 2, 3], [1, 2, 3]]) + + +def test_array_from_list_with_unsupported_dtype() -> None: + for dtype in supported_dtypes: + if dtype == float32: + continue + with pytest.raises(TypeError): + Array([1, 2, 3], dtype=dtype) + + +def test_array_from_af_array() -> None: + array1 = Array([1]) + array2 = Array(array1) + + assert array1.dtype == array2.dtype == float32 + assert array1.ndim == array2.ndim == 1 + assert array1.size == array2.size == 1 + assert array1.shape == array2.shape == (1,) + assert len(array1) == len(array2) == 1 + + +def test_array_from_int_without_shape() -> None: + with pytest.raises(TypeError): + Array(1) + + +def test_array_from_int_without_dtype() -> None: + with pytest.raises(TypeError): + Array(1, shape=(1,)) + +# def test_array_from_int_with_parameters() -> None: # BUG seg fault +# array = Array(1, shape=(1,), dtype=float32) + +# assert array.dtype == float32 +# assert array.ndim == 1 +# assert array.size == 1 +# assert array.shape == (1,) +# assert len(array) == 1 + + +def test_array_from_unsupported_type() -> None: + with pytest.raises(TypeError): + Array((5, 5)) # type: ignore[arg-type] + + with pytest.raises(TypeError): + Array({1: 2, 3: 4}) # type: ignore[arg-type] From c13a59f18ff5a90208a923738a236fc2823ef43d Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 24 Jan 2023 19:56:52 +0200 Subject: [PATCH 04/29] Fix CI --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e86d3fc90..8e420cfbc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ jobs: - name: Set up Python 3.10 uses: actions/setup-python@v1 with: - python-version: 3.10 + python-version: "3.10" - name: Install dependencies run: | From f0f57e8c269c6efa30669162cce7ff76d3efd19e Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 26 Jan 2023 21:05:29 +0200 Subject: [PATCH 05/29] Add arithmetic operators w/o tests --- arrayfire/array_api/_array_object.py | 158 +++++++++++++++++++-------- arrayfire/array_api/_utils.py | 9 +- 2 files changed, 114 insertions(+), 53 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 9e9c31d89..a18309dc6 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -3,14 +3,25 @@ import array as py_array import ctypes from dataclasses import dataclass +from typing import Any from arrayfire import backend, safe_call # TODO refactoring from arrayfire.array import _in_display_dims_limit # TODO refactoring -from ._dtypes import CShape, Dtype, c_dim_t, float32, supported_dtypes -from ._utils import Device, PointerSource, to_str +from ._dtypes import CShape, Dtype +from ._dtypes import bool as af_bool +from ._dtypes import c_dim_t +from ._dtypes import complex64 as af_complex64 +from ._dtypes import complex128 as af_complex128 +from ._dtypes import float32 as af_float32 +from ._dtypes import float64 as af_float64 +from ._dtypes import int64 as af_int64 +from ._dtypes import supported_dtypes +from ._dtypes import uint64 as af_uint64 +from ._utils import PointerSource, is_number, to_str ShapeType = tuple[int, ...] +_bcast_var = False # HACK, TODO replace for actual bcast_var after refactoring @dataclass @@ -40,7 +51,7 @@ def __init__( if dtype is None: _no_initial_dtype = True - dtype = float32 + dtype = af_float32 if x is None: if not shape: # shape is None or empty tuple @@ -134,15 +145,47 @@ def __neg__(self) -> Array: """ Return -self """ - # return 0 - self - raise NotImplementedError + return 0 - self def __add__(self, other: int | float | Array, /) -> Array: + # TODO discuss either we need to support complex and bool as other input type """ Return self + other. """ - # return _binary_func(self, other, backend.get().af_add) # TODO - raise NotImplementedError + return _process_c_function(self, other, backend.get().af_add) + + def __sub__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self - other. + """ + return _process_c_function(self, other, backend.get().af_sub) + + def __mul__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self * other. + """ + return _process_c_function(self, other, backend.get().af_mul) + + def __truediv__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self / other. + """ + return _process_c_function(self, other, backend.get().af_div) + + def __floordiv__(self, other: int | float | bool | complex | Array, /) -> Array: + return NotImplemented + + def __mod__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self % other. + """ + return _process_c_function(self, other, backend.get().af_mod) + + def __pow__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self ** other. + """ + return _process_c_function(self, other, backend.get().af_pow) @property def dtype(self) -> Dtype: @@ -151,7 +194,7 @@ def dtype(self) -> Dtype: return _c_api_value_to_dtype(out.value) @property - def device(self) -> Device: + def device(self) -> Any: raise NotImplementedError @property @@ -232,41 +275,66 @@ def _str_to_dtype(value: int) -> Dtype: raise TypeError("There is no supported dtype that matches passed dtype typecode.") -# TODO -# def _binary_func(lhs: int | float | Array, rhs: int | float | Array, c_func: Any) -> Array: # TODO replace Any -# out = Array() -# other = rhs - -# if is_number(rhs): -# ldims = _fill_dim4_tuple(lhs.shape) -# rty = implicit_dtype(rhs, lhs.type()) -# other = Array() -# other.arr = constant_array(rhs, ldims[0], ldims[1], ldims[2], ldims[3], rty.value) -# elif not isinstance(rhs, Array): -# raise TypeError("Invalid parameter to binary function") - -# safe_call(c_func(c_pointer(out.arr), lhs.arr, other.arr, _bcast_var.get())) - -# return out - - -# TODO replace candidate below -# def dim4_to_tuple(shape: ShapeType, default: int=1) -> ShapeType: -# assert(isinstance(dims, tuple)) - -# if (default is not None): -# assert(is_number(default)) - -# out = [default]*4 - -# for i, dim in enumerate(dims): -# out[i] = dim - -# return tuple(out) - -# def _fill_dim4_tuple(shape: ShapeType) -> tuple[int, ...]: -# out = tuple([1 if value is None else value for value in shape]) -# if len(out) == 4: -# return out -# return out + (1,)*(4-len(out)) +def _process_c_function( + target: Array, other: int | float | bool | complex | Array, c_function: Any) -> Array: + out = Array() + + if isinstance(other, Array): + safe_call(c_function(ctypes.pointer(out.arr), target.arr, other.arr, _bcast_var)) + elif is_number(other): + target_c_shape = CShape(*target.shape) + other_dtype = _implicit_dtype(other, target.dtype) + other_array = _constant_array(other, target_c_shape, other_dtype) + safe_call(c_function(ctypes.pointer(out.arr), target.arr, other_array.arr, _bcast_var)) + else: + raise TypeError(f"{type(other)} is not supported and can not be passed to C binary function.") + + return out + + +def _implicit_dtype(value: int | float | bool | complex, array_dtype: Dtype) -> Dtype: + if isinstance(value, bool): + value_dtype = af_bool + if isinstance(value, int): + value_dtype = af_int64 + elif isinstance(value, float): + value_dtype = af_float64 + elif isinstance(value, complex): + value_dtype = af_complex128 + else: + raise TypeError(f"{type(value)} is not supported and can not be converted to af.Dtype.") + + if not (array_dtype == af_float32 or array_dtype == af_complex64): + return value_dtype + + if value_dtype == af_float64: + return af_float32 + + if value_dtype == af_complex128: + return af_complex64 + + return value_dtype + + +def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: Dtype) -> Array: + out = Array() + + if isinstance(value, complex): + if dtype != af_complex64 and dtype != af_complex128: + dtype = af_complex64 + + safe_call(backend.get().af_constant_complex( + ctypes.pointer(out.arr), ctypes.c_double(value.real), ctypes.c_double(value.imag), 4, + ctypes.pointer(shape.c_array), dtype)) + elif dtype == af_int64: + safe_call(backend.get().af_constant_long( + ctypes.pointer(out.arr), ctypes.c_longlong(value.real), 4, ctypes.pointer(shape.c_array))) + elif dtype == af_uint64: + safe_call(backend.get().af_constant_ulong( + ctypes.pointer(out.arr), ctypes.c_ulonglong(value.real), 4, ctypes.pointer(shape.c_array))) + else: + safe_call(backend.get().af_constant( + ctypes.pointer(out.arr), ctypes.c_double(value), 4, ctypes.pointer(shape.c_array), dtype)) + + return out diff --git a/arrayfire/array_api/_utils.py b/arrayfire/array_api/_utils.py index e235480c2..67b06f253 100644 --- a/arrayfire/array_api/_utils.py +++ b/arrayfire/array_api/_utils.py @@ -1,13 +1,6 @@ import ctypes import enum import numbers -from typing import Any - - -class Device(enum.Enum): - # HACK. TODO make it real - cpu = "cpu" - gpu = "gpu" class PointerSource(enum.Enum): @@ -23,5 +16,5 @@ def to_str(c_str: ctypes.c_char_p) -> str: return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] -def is_number(number: Any) -> bool: +def is_number(number: int | float | bool | complex) -> bool: return isinstance(number, numbers.Number) From 8cef7744e454247cab1c1f36135bd71271784715 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 27 Jan 2023 19:28:39 +0200 Subject: [PATCH 06/29] Fix array init bug. Add __getitem__. Change pytest for active debug mode --- arrayfire/array_api/_array_object.py | 64 ++++++++++++++----- arrayfire/array_api/pytest.ini | 2 +- .../array_api/tests/test_array_object.py | 18 ++++++ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index a18309dc6..0d0c76a8a 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -5,8 +5,9 @@ from dataclasses import dataclass from typing import Any -from arrayfire import backend, safe_call # TODO refactoring -from arrayfire.array import _in_display_dims_limit # TODO refactoring +from arrayfire import backend, safe_call # TODO refactor +from arrayfire.algorithm import count # TODO refactor +from arrayfire.array import _get_indices, _in_display_dims_limit # TODO refactor from ._dtypes import CShape, Dtype from ._dtypes import bool as af_bool @@ -37,15 +38,15 @@ class Array: # arrayfire's __radd__() instead of numpy's __add__() __array_priority__ = 30 - # Initialisation - arr = ctypes.c_void_p(0) - def __init__( self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, dtype: None | Dtype = None, pointer_source: PointerSource = PointerSource.host, shape: None | ShapeType = None, offset: None | ctypes._SimpleCData[int] = None, strides: None | ShapeType = None) -> None: _no_initial_dtype = False # HACK, FIXME + # Initialise array object + self.arr = ctypes.c_void_p(0) + if isinstance(dtype, str): dtype = _str_to_dtype(dtype) @@ -127,7 +128,7 @@ def __str__(self) -> str: # FIXME if not _in_display_dims_limit(self.shape): return _metadata_string(self.dtype, self.shape) - return _metadata_string(self.dtype) + self._as_str() + return _metadata_string(self.dtype) + _array_as_str(self) def __repr__(self) -> str: # FIXME return _metadata_string(self.dtype, self.shape) @@ -173,6 +174,7 @@ def __truediv__(self, other: int | float | bool | complex | Array, /) -> Array: return _process_c_function(self, other, backend.get().af_div) def __floordiv__(self, other: int | float | bool | complex | Array, /) -> Array: + # TODO return NotImplemented def __mod__(self, other: int | float | bool | complex | Array, /) -> Array: @@ -187,6 +189,25 @@ def __pow__(self, other: int | float | bool | complex | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_pow) + def __matmul__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + + def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: + # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array + # TODO: refactor + out = Array() + ndims = self.ndim + + if isinstance(key, Array) and key == af_bool.c_api_value: + ndims = 1 + if count(key) == 0: + return out + + safe_call(backend.get().af_index_gen( + ctypes.pointer(out.arr), self.arr, c_dim_t(ndims), _get_indices(key).pointer)) + return out + @property def dtype(self) -> Dtype: out = ctypes.c_int() @@ -234,13 +255,23 @@ def shape(self) -> ShapeType: ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), self.arr)) return (d0.value, d1.value, d2.value, d3.value)[:self.ndim] # Skip passing None values - def _as_str(self) -> str: - arr_str = ctypes.c_char_p(0) - # FIXME add description to passed arguments - safe_call(backend.get().af_array_to_string(ctypes.pointer(arr_str), "", self.arr, 4, True)) - py_str = to_str(arr_str) - safe_call(backend.get().af_free_host(arr_str)) - return py_str + def scalar(self) -> int | float | bool | complex: + """ + Return the first element of the array + """ + # BUG seg fault on empty array + out = self.dtype.c_type() + safe_call(backend.get().af_get_scalar(ctypes.pointer(out), self.arr)) + return out.value # type: ignore[no-any-return] # FIXME + + +def _array_as_str(array: Array) -> str: + arr_str = ctypes.c_char_p(0) + # FIXME add description to passed arguments + safe_call(backend.get().af_array_to_string(ctypes.pointer(arr_str), "", array.arr, 4, True)) + py_str = to_str(arr_str) + safe_call(backend.get().af_free_host(arr_str)) + return py_str def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: @@ -283,9 +314,8 @@ def _process_c_function( if isinstance(other, Array): safe_call(c_function(ctypes.pointer(out.arr), target.arr, other.arr, _bcast_var)) elif is_number(other): - target_c_shape = CShape(*target.shape) other_dtype = _implicit_dtype(other, target.dtype) - other_array = _constant_array(other, target_c_shape, other_dtype) + other_array = _constant_array(other, CShape(*target.shape), other_dtype) safe_call(c_function(ctypes.pointer(out.arr), target.arr, other_array.arr, _bcast_var)) else: raise TypeError(f"{type(other)} is not supported and can not be passed to C binary function.") @@ -326,7 +356,7 @@ def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: D safe_call(backend.get().af_constant_complex( ctypes.pointer(out.arr), ctypes.c_double(value.real), ctypes.c_double(value.imag), 4, - ctypes.pointer(shape.c_array), dtype)) + ctypes.pointer(shape.c_array), dtype.c_api_value)) elif dtype == af_int64: safe_call(backend.get().af_constant_long( ctypes.pointer(out.arr), ctypes.c_longlong(value.real), 4, ctypes.pointer(shape.c_array))) @@ -335,6 +365,6 @@ def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: D ctypes.pointer(out.arr), ctypes.c_ulonglong(value.real), 4, ctypes.pointer(shape.c_array))) else: safe_call(backend.get().af_constant( - ctypes.pointer(out.arr), ctypes.c_double(value), 4, ctypes.pointer(shape.c_array), dtype)) + ctypes.pointer(out.arr), ctypes.c_double(value), 4, ctypes.pointer(shape.c_array), dtype.c_api_value)) return out diff --git a/arrayfire/array_api/pytest.ini b/arrayfire/array_api/pytest.ini index 43feed854..7fd828bec 100644 --- a/arrayfire/array_api/pytest.ini +++ b/arrayfire/array_api/pytest.ini @@ -1,4 +1,4 @@ [pytest] -addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --isort ./arrayfire/array_api +addopts = --cache-clear --cov=./arrayfire/array_api --flake8 --isort -s ./arrayfire/array_api console_output_style = classic markers = mypy diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index e52f83556..028e1fe6a 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -93,3 +93,21 @@ def test_array_from_unsupported_type() -> None: with pytest.raises(TypeError): Array({1: 2, 3: 4}) # type: ignore[arg-type] + + +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 + + # TODO add more tests for different dtypes + + +# def test_array_sum() -> None: # BUG no element-wise adding +# array = Array([1, 2, 3]) +# res = array + 1 +# assert res.scalar() == 2 +# assert res.scalar() == 3 +# assert res.scalar() == 4 From a4c7ac9cf45d5b8a92f3c02fcad81cfb2034df4e Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 27 Jan 2023 19:37:05 +0200 Subject: [PATCH 07/29] Add reflected arithmetic and array operators --- arrayfire/array_api/_array_object.py | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 0d0c76a8a..e51207db0 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -193,6 +193,51 @@ def __matmul__(self, other: Array, /) -> Array: # TODO return NotImplemented + def __radd__(self, other: Array, /) -> Array: + # TODO discuss either we need to support complex and bool as other input type + """ + Return other + self. + """ + return _process_c_function(other, self, backend.get().af_add) + + def __rsub__(self, other: Array, /) -> Array: + """ + Return other - self. + """ + return _process_c_function(other, self, backend.get().af_sub) + + def __rmul__(self, other: Array, /) -> Array: + """ + Return other * self. + """ + return _process_c_function(other, self, backend.get().af_mul) + + def __rtruediv__(self, other: Array, /) -> Array: + """ + Return other / self. + """ + return _process_c_function(other, self, backend.get().af_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.get().af_mod) + + def __rpow__(self, other: Array, /) -> Array: + """ + Return other ** self. + """ + return _process_c_function(other, self, backend.get().af_pow) + + def __rmatmul__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array # TODO: refactor From 41405279484aeb01d67db5fb39b2fc59cbe8f8e6 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 16:28:09 +0200 Subject: [PATCH 08/29] Place TODO for repr --- arrayfire/array_api/_array_object.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index e51207db0..f5fcf4744 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -131,7 +131,9 @@ def __str__(self) -> str: # FIXME return _metadata_string(self.dtype) + _array_as_str(self) def __repr__(self) -> str: # FIXME - return _metadata_string(self.dtype, self.shape) + # return _metadata_string(self.dtype, self.shape) + # TODO change the look of array representation. E.g., like np.array + return _array_as_str(self) def __len__(self) -> int: return self.shape[0] if self.shape else 0 # type: ignore[return-value] From 4374d9397aaa3446ecf4f3d96529ff49cbd2f31c Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 16:53:23 +0200 Subject: [PATCH 09/29] Add bitwise operators. Add in-place operators. Add missing reflected operators --- arrayfire/array_api/_array_object.py | 204 ++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index f5fcf4744..d6b5a07b9 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -87,7 +87,7 @@ def __init__( if _no_initial_dtype: raise TypeError("Expected to receive the initial dtype due to the x being a data pointer.") - _type_char = dtype.typecode + _type_char = dtype.typecode # type: ignore[assignment] # FIXME else: raise TypeError("Passed object x is an object of unsupported class.") @@ -138,6 +138,8 @@ def __repr__(self) -> str: # FIXME def __len__(self) -> int: return self.shape[0] if self.shape else 0 # type: ignore[return-value] + # Arithmetic Operators + def __pos__(self) -> Array: """ Return +self @@ -148,7 +150,7 @@ def __neg__(self) -> Array: """ Return -self """ - return 0 - self + return 0 - self # type: ignore[no-any-return, operator] # FIXME def __add__(self, other: int | float | Array, /) -> Array: # TODO discuss either we need to support complex and bool as other input type @@ -191,10 +193,92 @@ def __pow__(self, other: int | float | bool | complex | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_pow) + # Array Operators + def __matmul__(self, other: Array, /) -> Array: # TODO return NotImplemented + # Bitwise Operators + + def __invert__(self) -> Array: + """ + Return ~self. + """ + out = Array() + safe_call(backend.get().af_bitnot(ctypes.pointer(out.arr), self.arr)) + return out + + def __and__(self, other: int | bool | Array, /) -> Array: + """ + Return self & other. + """ + return _process_c_function(self, other, backend.get().af_bitand) + + def __or__(self, other: int | bool | Array, /) -> Array: + """ + Return self | other. + """ + return _process_c_function(self, other, backend.get().af_bitor) + + def __xor__(self, other: int | bool | Array, /) -> Array: + """ + Return self ^ other. + """ + return _process_c_function(self, other, backend.get().af_bitxor) + + def __lshift__(self, other: int | Array, /) -> Array: + """ + Return self << other. + """ + return _process_c_function(self, other, backend.get().af_bitshiftl) + + def __rshift__(self, other: int | Array, /) -> Array: + """ + Return self >> other. + """ + return _process_c_function(self, other, backend.get().af_bitshiftr) + + # Comparison Operators + + def __lt__(self, other: int | float | Array, /) -> Array: + """ + Return self < other. + """ + return _process_c_function(self, other, backend.get().af_lt) + + def __le__(self, other: int | float | Array, /) -> Array: + """ + Return self <= other. + """ + return _process_c_function(self, other, backend.get().af_le) + + def __gt__(self, other: int | float | Array, /) -> Array: + """ + Return self > other. + """ + return _process_c_function(self, other, backend.get().af_gt) + + def __ge__(self, other: int | float | Array, /) -> Array: + """ + Return self >= other. + """ + return _process_c_function(self, other, backend.get().af_ge) + + def __eq__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME + """ + Return self == other. + """ + return _process_c_function(self, other, backend.get().af_eq) + + def __ne__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME + """ + Return self != other. + """ + return _process_c_function(self, other, backend.get().af_neq) + + # Reflected Arithmetic Operators + def __radd__(self, other: Array, /) -> Array: # TODO discuss either we need to support complex and bool as other input type """ @@ -236,10 +320,125 @@ def __rpow__(self, other: Array, /) -> Array: """ return _process_c_function(other, self, backend.get().af_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.get().af_bitand) + + def __ror__(self, other: Array, /) -> Array: + """ + Return other & self. + """ + return _process_c_function(other, self, backend.get().af_bitor) + + def __rxor__(self, other: Array, /) -> Array: + """ + Return other ^ self. + """ + return _process_c_function(other, self, backend.get().af_bitxor) + + def __rlshift__(self, other: Array, /) -> Array: + """ + Return other << self. + """ + return _process_c_function(other, self, backend.get().af_bitshiftl) + + def __rrshift__(self, other: Array, /) -> Array: + """ + Return other >> self. + """ + return _process_c_function(other, self, backend.get().af_bitshiftr) + + # In-place Arithmetic Operators + + def __iadd__(self, other: 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.get().af_add) + + def __isub__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self -= other. + """ + return _process_c_function(self, other, backend.get().af_sub) + + def __imul__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self *= other. + """ + return _process_c_function(self, other, backend.get().af_mul) + + def __itruediv__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self /= other. + """ + return _process_c_function(self, other, backend.get().af_div) + + def __ifloordiv__(self, other: int | float | bool | complex | Array, /) -> Array: + # TODO + return NotImplemented + + def __imod__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self %= other. + """ + return _process_c_function(self, other, backend.get().af_mod) + + def __ipow__(self, other: int | float | bool | complex | Array, /) -> Array: + """ + Return self **= other. + """ + return _process_c_function(self, other, backend.get().af_pow) + + # In-place Array Operators + + def __imatmul__(self, other: Array, /) -> Array: + # TODO + return NotImplemented + + # In-place Bitwise Operators + + def __iand__(self, other: int | bool | Array, /) -> Array: + """ + Return self &= other. + """ + return _process_c_function(self, other, backend.get().af_bitand) + + def __ior__(self, other: int | bool | Array, /) -> Array: + """ + Return self |= other. + """ + return _process_c_function(self, other, backend.get().af_bitor) + + def __ixor__(self, other: int | bool | Array, /) -> Array: + """ + Return self ^= other. + """ + return _process_c_function(self, other, backend.get().af_bitxor) + + def __ilshift__(self, other: int | Array, /) -> Array: + """ + Return self <<= other. + """ + return _process_c_function(self, other, backend.get().af_bitshiftl) + + def __irshift__(self, other: int | Array, /) -> Array: + """ + Return self >>= other. + """ + return _process_c_function(self, other, backend.get().af_bitshiftr) + def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array # TODO: refactor @@ -405,6 +604,7 @@ def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: D ctypes.pointer(out.arr), ctypes.c_double(value.real), ctypes.c_double(value.imag), 4, ctypes.pointer(shape.c_array), dtype.c_api_value)) elif dtype == af_int64: + # TODO discuss workaround for passing float to ctypes safe_call(backend.get().af_constant_long( ctypes.pointer(out.arr), ctypes.c_longlong(value.real), 4, ctypes.pointer(shape.c_array))) elif dtype == af_uint64: From 5a29ffa69fbd59b6f8cc972faae1cfa7487f5d9e Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 16:55:34 +0200 Subject: [PATCH 10/29] Fix tests --- arrayfire/array_api/tests/test_array_object.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 028e1fe6a..769d5f175 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -105,9 +105,9 @@ def test_array_getitem() -> None: # TODO add more tests for different dtypes -# def test_array_sum() -> None: # BUG no element-wise adding -# array = Array([1, 2, 3]) -# res = array + 1 -# assert res.scalar() == 2 -# assert res.scalar() == 3 -# assert res.scalar() == 4 +def test_array_sum() -> None: + array = Array([1, 2, 3]) + res = array + 1 + assert res[0].scalar() == 2 + assert res[1].scalar() == 3 + assert res[2].scalar() == 4 From 4187b27b9b39df811f5069184246b1572ce61667 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 17:23:28 +0200 Subject: [PATCH 11/29] Add tests for arithmetic operators --- arrayfire/array_api/_array_object.py | 12 +- .../array_api/tests/test_array_object.py | 105 ++++++++++++++++++ 2 files changed, 111 insertions(+), 6 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index d6b5a07b9..308f1e5ea 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -159,35 +159,35 @@ def __add__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_add) - def __sub__(self, other: int | float | bool | complex | Array, /) -> Array: + def __sub__(self, other: int | float | Array, /) -> Array: """ Return self - other. """ return _process_c_function(self, other, backend.get().af_sub) - def __mul__(self, other: int | float | bool | complex | Array, /) -> Array: + def __mul__(self, other: int | float | Array, /) -> Array: """ Return self * other. """ return _process_c_function(self, other, backend.get().af_mul) - def __truediv__(self, other: int | float | bool | complex | Array, /) -> Array: + def __truediv__(self, other: int | float | Array, /) -> Array: """ Return self / other. """ return _process_c_function(self, other, backend.get().af_div) - def __floordiv__(self, other: int | float | bool | complex | Array, /) -> Array: + def __floordiv__(self, other: int | float | Array, /) -> Array: # TODO return NotImplemented - def __mod__(self, other: int | float | bool | complex | Array, /) -> Array: + def __mod__(self, other: int | float | Array, /) -> Array: """ Return self % other. """ return _process_c_function(self, other, backend.get().af_mod) - def __pow__(self, other: int | float | bool | complex | Array, /) -> Array: + def __pow__(self, other: int | float | Array, /) -> Array: """ Return self ** other. """ diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 769d5f175..c139f57b6 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -111,3 +111,108 @@ def test_array_sum() -> None: assert res[0].scalar() == 2 assert res[1].scalar() == 3 assert res[2].scalar() == 4 + + res = array + 1.5 + assert res[0].scalar() == 2.5 + assert res[1].scalar() == 3.5 + assert res[2].scalar() == 4.5 + + res = array + Array([9, 9, 9]) + assert res[0].scalar() == 10 + assert res[1].scalar() == 11 + assert res[2].scalar() == 12 + + +def test_array_sub() -> None: + array = Array([1, 2, 3]) + res = array - 1 + assert res[0].scalar() == 0 + assert res[1].scalar() == 1 + assert res[2].scalar() == 2 + + res = array - 1.5 + assert res[0].scalar() == -0.5 + assert res[1].scalar() == 0.5 + assert res[2].scalar() == 1.5 + + res = array - Array([9, 9, 9]) + assert res[0].scalar() == -8 + assert res[1].scalar() == -7 + assert res[2].scalar() == -6 + + +def test_array_mul() -> None: + array = Array([1, 2, 3]) + res = array * 2 + assert res[0].scalar() == 2 + assert res[1].scalar() == 4 + assert res[2].scalar() == 6 + + res = array * 1.5 + assert res[0].scalar() == 1.5 + assert res[1].scalar() == 3 + assert res[2].scalar() == 4.5 + + res = array * Array([9, 9, 9]) + assert res[0].scalar() == 9 + assert res[1].scalar() == 18 + assert res[2].scalar() == 27 + + +def test_array_truediv() -> None: + array = Array([1, 2, 3]) + res = array / 2 + assert res[0].scalar() == 0.5 + assert res[1].scalar() == 1 + assert res[2].scalar() == 1.5 + + res = array / 1.5 + assert round(res[0].scalar(), 5) == 0.66667 # type: ignore[arg-type] + assert round(res[1].scalar(), 5) == 1.33333 # type: ignore[arg-type] + assert res[2].scalar() == 2 + + res = array / Array([2, 2, 2]) + assert res[0].scalar() == 0.5 + assert res[1].scalar() == 1 + assert res[2].scalar() == 1.5 + + +def test_array_floordiv() -> None: + # TODO add test after implementation of __floordiv__ + pass + + +def test_array_mod() -> None: + array = Array([1, 2, 3]) + res = array % 2 + assert res[0].scalar() == 1 + assert res[1].scalar() == 0 + assert res[2].scalar() == 1 + + res = array % 1.5 + assert res[0].scalar() == 1.0 + assert res[1].scalar() == 0.5 + assert res[2].scalar() == 0.0 + + res = array % Array([9, 9, 9]) + assert res[0].scalar() == 1.0 + assert res[1].scalar() == 2.0 + assert res[2].scalar() == 3.0 + + +def test_array_pow() -> None: + array = Array([1, 2, 3]) + res = array ** 2 + assert res[0].scalar() == 1 + assert res[1].scalar() == 4 + assert res[2].scalar() == 9 + + res = array ** 1.5 + assert res[0].scalar() == 1 + assert round(res[1].scalar(), 5) == 2.82843 # type: ignore[arg-type] + assert round(res[2].scalar(), 5) == 5.19615 # type: ignore[arg-type] + + res = array ** Array([9, 9, 9]) + assert res[0].scalar() == 1 + assert res[1].scalar() == 512 + assert res[2].scalar() == 19683 From cdb7a92f3ae8789268123e8d7ca4c32097a749a7 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 21:39:17 +0200 Subject: [PATCH 12/29] Added to_list and to_ctypes_array --- arrayfire/array_api/_array_object.py | 71 +++++++++++++++++-- .../array_api/tests/test_array_object.py | 15 +++- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 308f1e5ea..1e6d24216 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -124,19 +124,20 @@ def __init__( ctypes.pointer(_cshape.c_array), ctypes.pointer(strides_cshape), dtype.c_api_value, pointer_source.value)) - def __str__(self) -> str: # 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) + _array_as_str(self) - def __repr__(self) -> str: # FIXME + def __repr__(self) -> str: # return _metadata_string(self.dtype, self.shape) # TODO change the look of array representation. E.g., like np.array return _array_as_str(self) def __len__(self) -> int: - return self.shape[0] if self.shape else 0 # type: ignore[return-value] + return self.shape[0] if self.shape else 0 # Arithmetic Operators @@ -475,7 +476,7 @@ def T(self) -> Array: raise NotImplementedError @property - def size(self) -> None | int: + def size(self) -> int: # NOTE previously - elements() out = c_dim_t(0) safe_call(backend.get().af_get_elements(ctypes.pointer(out), self.arr)) @@ -483,9 +484,9 @@ def size(self) -> None | int: @property def ndim(self) -> int: - nd = ctypes.c_uint(0) - safe_call(backend.get().af_get_numdims(ctypes.pointer(nd), self.arr)) - return nd.value + out = ctypes.c_uint(0) + safe_call(backend.get().af_get_numdims(ctypes.pointer(out), self.arr)) + return out.value @property def shape(self) -> ShapeType: @@ -510,6 +511,62 @@ def scalar(self) -> int | float | bool | complex: safe_call(backend.get().af_get_scalar(ctypes.pointer(out), self.arr)) return out.value # type: ignore[no-any-return] # FIXME + def is_empty(self) -> bool: + """ + Check if the array is empty i.e. it has no elements. + """ + out = ctypes.c_bool() + safe_call(backend.get().af_is_empty(ctypes.pointer(out), self.arr)) + return out.value + + def to_list(self, row_major: bool = False) -> list: # FIXME return typings + if self.is_empty(): + return [] + + array = _reorder(self) if row_major else self + ctypes_array = _get_ctypes_array(array) + + 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 _get_ctypes_array(array) + + +def _get_ctypes_array(array: Array) -> ctypes.Array: + c_shape = array.dtype.c_type * array.size + ctypes_array = c_shape() + safe_call(backend.get().af_get_data_ptr(ctypes.pointer(ctypes_array), array.arr)) + return ctypes_array + + +def _reorder(array: Array) -> Array: + """ + Returns a reordered array to help interoperate with row major formats. + """ + if array.ndim == 1: + return array + + out = Array() + c_shape = CShape(*(tuple(reversed(range(array.ndim))) + tuple(range(array.ndim, 4)))) + safe_call(backend.get().af_reorder(ctypes.pointer(out.arr), array.arr, *c_shape)) + return out + def _array_as_str(array: Array) -> str: arr_str = ctypes.c_char_p(0) diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index c139f57b6..7eb0a78e8 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -3,6 +3,8 @@ from arrayfire.array_api import Array, float32, int16 from arrayfire.array_api._dtypes import supported_dtypes +# TODO change separated methods with setup and teardown to avoid code duplication + def test_empty_array() -> None: array = Array() @@ -105,7 +107,13 @@ def test_array_getitem() -> None: # TODO add more tests for different dtypes -def test_array_sum() -> None: +def test_array_to_list() -> None: + # TODO add test of to_ctypes_array + assert Array([1, 2, 3]).to_list() == [1, 2, 3] + assert Array().to_list() == [] + + +def test_array_add() -> None: array = Array([1, 2, 3]) res = array + 1 assert res[0].scalar() == 2 @@ -123,6 +131,11 @@ def test_array_sum() -> None: assert res[2].scalar() == 12 +def test_array_add_raises_type_error() -> None: + with pytest.raises(TypeError): + Array([1, 2, 3]) + "15" # type: ignore[operator] + + def test_array_sub() -> None: array = Array([1, 2, 3]) res = array - 1 From 9c0435a3f7a2b77ccf6aa2a8d75747428aa61ab2 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 28 Jan 2023 21:44:51 +0200 Subject: [PATCH 13/29] Fix bug when scalar is empty returns None --- arrayfire/array_api/_array_object.py | 6 ++++-- .../array_api/tests/test_array_object.py | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 1e6d24216..36fb2cb69 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -502,11 +502,13 @@ def shape(self) -> ShapeType: ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), self.arr)) return (d0.value, d1.value, d2.value, d3.value)[:self.ndim] # Skip passing None values - def scalar(self) -> int | float | bool | complex: + def scalar(self) -> None | int | float | bool | complex: """ Return the first element of the array """ - # BUG seg fault on empty array + if self.is_empty(): + return None + out = self.dtype.c_type() safe_call(backend.get().af_get_scalar(ctypes.pointer(out), self.arr)) return out.value # type: ignore[no-any-return] # FIXME diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 7eb0a78e8..be80365b1 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -107,10 +107,24 @@ def test_array_getitem() -> None: # TODO add more tests for different dtypes +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: - # TODO add test of to_ctypes_array - assert Array([1, 2, 3]).to_list() == [1, 2, 3] - assert Array().to_list() == [] + 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() == [] def test_array_add() -> None: From 769c16cbcb6dcbd88dccbb8d635d522732a3b9f3 Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 29 Jan 2023 02:01:13 +0200 Subject: [PATCH 14/29] Fix typing in array object. Add tests --- arrayfire/array_api/_array_object.py | 9 ++-- .../array_api/tests/test_array_object.py | 49 +++++++++++++++++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 36fb2cb69..6e13eb330 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -39,16 +39,17 @@ class Array: __array_priority__ = 30 def __init__( - self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, dtype: None | Dtype = None, - pointer_source: PointerSource = PointerSource.host, shape: None | ShapeType = None, - offset: None | ctypes._SimpleCData[int] = None, strides: None | ShapeType = None) -> None: + self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, + dtype: None | Dtype | str = None, shape: None | ShapeType = None, + pointer_source: PointerSource = PointerSource.host, offset: None | ctypes._SimpleCData[int] = None, + strides: None | ShapeType = None) -> None: _no_initial_dtype = False # HACK, FIXME # Initialise array object self.arr = ctypes.c_void_p(0) if isinstance(dtype, str): - dtype = _str_to_dtype(dtype) + dtype = _str_to_dtype(dtype) # type: ignore[arg-type] if dtype is None: _no_initial_dtype = True diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index be80365b1..dc3b31de0 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -1,12 +1,15 @@ +import array as pyarray + import pytest from arrayfire.array_api import Array, float32, int16 from arrayfire.array_api._dtypes import supported_dtypes # TODO change separated methods with setup and teardown to avoid code duplication +# TODO add tests for array arguments: device, offset, strides -def test_empty_array() -> None: +def test_create_empty_array() -> None: array = Array() assert array.dtype == float32 @@ -16,7 +19,7 @@ def test_empty_array() -> None: assert len(array) == 0 -def test_empty_array_with_nonempty_dtype() -> None: +def test_create_empty_array_with_nonempty_dtype() -> None: array = Array(dtype=int16) assert array.dtype == int16 @@ -26,7 +29,32 @@ def test_empty_array_with_nonempty_dtype() -> None: assert len(array) == 0 -def test_empty_array_with_nonempty_shape() -> None: +def test_create_empty_array_with_str_dtype() -> None: + array = Array(dtype="short int") + + assert array.dtype == int16 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 + + +def test_create_empty_array_with_literal_dtype() -> None: + array = Array(dtype="h") + + assert array.dtype == int16 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 + + +def test_create_empty_array_with_not_matching_str_dtype() -> None: + with pytest.raises(TypeError): + Array(dtype="hello world") + + +def test_create_empty_array_with_nonempty_shape() -> None: array = Array(shape=(2, 3)) assert array.dtype == float32 @@ -36,7 +64,7 @@ def test_empty_array_with_nonempty_shape() -> None: assert len(array) == 2 -def test_array_from_1d_list() -> None: +def test_create_array_from_1d_list() -> None: array = Array([1, 2, 3]) assert array.dtype == float32 @@ -46,11 +74,22 @@ def test_array_from_1d_list() -> None: assert len(array) == 3 -def test_array_from_2d_list() -> None: +def test_create_array_from_2d_list() -> None: with pytest.raises(TypeError): Array([[1, 2, 3], [1, 2, 3]]) +def test_create_array_from_pyarray() -> None: + py_array = pyarray.array("f", [1, 2, 3]) + array = Array(py_array) + + assert array.dtype == float32 + assert array.ndim == 1 + assert array.size == 3 + assert array.shape == (3,) + assert len(array) == 3 + + def test_array_from_list_with_unsupported_dtype() -> None: for dtype in supported_dtypes: if dtype == float32: From fb27e469f42453e3e4c18b1125ba546f280ff422 Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 29 Jan 2023 03:16:06 +0200 Subject: [PATCH 15/29] Change tests and found bug with reflected operators --- arrayfire/array_api/_array_object.py | 27 +- .../array_api/tests/test_array_object.py | 392 +++++++++++++----- 2 files changed, 297 insertions(+), 122 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 6e13eb330..bb676c3fd 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -36,7 +36,7 @@ class Array: # Setting to such a high value should make sure that arrayfire has priority over # other classes, ensuring that e.g. numpy.float32(1)*arrayfire.randu(3) is handled by # arrayfire's __radd__() instead of numpy's __add__() - __array_priority__ = 30 + __array_priority__ = 30 # TODO discuss its purpose def __init__( self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, @@ -286,25 +286,25 @@ def __radd__(self, other: Array, /) -> Array: """ Return other + self. """ - return _process_c_function(other, self, backend.get().af_add) + return _process_c_function(self, other, backend.get().af_add) def __rsub__(self, other: Array, /) -> Array: """ Return other - self. """ - return _process_c_function(other, self, backend.get().af_sub) + return _process_c_function(self, other, backend.get().af_sub) def __rmul__(self, other: Array, /) -> Array: """ Return other * self. """ - return _process_c_function(other, self, backend.get().af_mul) + return _process_c_function(self, other, backend.get().af_mul) def __rtruediv__(self, other: Array, /) -> Array: """ Return other / self. """ - return _process_c_function(other, self, backend.get().af_div) + return _process_c_function(self, other, backend.get().af_div) def __rfloordiv__(self, other: Array, /) -> Array: # TODO @@ -314,13 +314,13 @@ def __rmod__(self, other: Array, /) -> Array: """ Return other / self. """ - return _process_c_function(other, self, backend.get().af_mod) + return _process_c_function(self, other, backend.get().af_mod) def __rpow__(self, other: Array, /) -> Array: """ Return other ** self. """ - return _process_c_function(other, self, backend.get().af_pow) + return _process_c_function(self, other, backend.get().af_pow) # Reflected Array Operators @@ -334,31 +334,31 @@ def __rand__(self, other: Array, /) -> Array: """ Return other & self. """ - return _process_c_function(other, self, backend.get().af_bitand) + return _process_c_function(self, other, backend.get().af_bitand) def __ror__(self, other: Array, /) -> Array: """ Return other & self. """ - return _process_c_function(other, self, backend.get().af_bitor) + return _process_c_function(self, other, backend.get().af_bitor) def __rxor__(self, other: Array, /) -> Array: """ Return other ^ self. """ - return _process_c_function(other, self, backend.get().af_bitxor) + return _process_c_function(self, other, backend.get().af_bitxor) def __rlshift__(self, other: Array, /) -> Array: """ Return other << self. """ - return _process_c_function(other, self, backend.get().af_bitshiftl) + return _process_c_function(self, other, backend.get().af_bitshiftl) def __rrshift__(self, other: Array, /) -> Array: """ Return other >> self. """ - return _process_c_function(other, self, backend.get().af_bitshiftr) + return _process_c_function(self, other, backend.get().af_bitshiftr) # In-place Arithmetic Operators @@ -617,6 +617,9 @@ def _process_c_function( target: Array, other: int | float | bool | complex | Array, c_function: Any) -> Array: out = Array() + # TODO discuss the difference between binary_func and binary_funcr + # because implementation looks like exectly the same. + # consider chaging to __iadd__ = __radd__ = __add__ interfce if no difference if isinstance(other, Array): safe_call(c_function(ctypes.pointer(out.arr), target.arr, other.arr, _bcast_var)) elif is_number(other): diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index dc3b31de0..4f50019ce 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -1,4 +1,5 @@ import array as pyarray +from typing import Any import pytest @@ -7,6 +8,7 @@ # TODO change separated methods with setup and teardown to avoid code duplication # TODO add tests for array arguments: device, offset, strides +# TODO add tests for all supported dtypes on initialisation def test_create_empty_array() -> None: @@ -166,119 +168,289 @@ def test_array_to_list_is_empty() -> None: assert array.to_list() == [] -def test_array_add() -> None: - array = Array([1, 2, 3]) - res = array + 1 - assert res[0].scalar() == 2 - assert res[1].scalar() == 3 - assert res[2].scalar() == 4 - - res = array + 1.5 - assert res[0].scalar() == 2.5 - assert res[1].scalar() == 3.5 - assert res[2].scalar() == 4.5 - - res = array + Array([9, 9, 9]) - assert res[0].scalar() == 10 - assert res[1].scalar() == 11 - assert res[2].scalar() == 12 - - -def test_array_add_raises_type_error() -> None: - with pytest.raises(TypeError): - Array([1, 2, 3]) + "15" # type: ignore[operator] - - -def test_array_sub() -> None: - array = Array([1, 2, 3]) - res = array - 1 - assert res[0].scalar() == 0 - assert res[1].scalar() == 1 - assert res[2].scalar() == 2 - - res = array - 1.5 - assert res[0].scalar() == -0.5 - assert res[1].scalar() == 0.5 - assert res[2].scalar() == 1.5 - - res = array - Array([9, 9, 9]) - assert res[0].scalar() == -8 - assert res[1].scalar() == -7 - assert res[2].scalar() == -6 - - -def test_array_mul() -> None: - array = Array([1, 2, 3]) - res = array * 2 - assert res[0].scalar() == 2 - assert res[1].scalar() == 4 - assert res[2].scalar() == 6 - - res = array * 1.5 - assert res[0].scalar() == 1.5 - assert res[1].scalar() == 3 - assert res[2].scalar() == 4.5 - - res = array * Array([9, 9, 9]) - assert res[0].scalar() == 9 - assert res[1].scalar() == 18 - assert res[2].scalar() == 27 - - -def test_array_truediv() -> None: - array = Array([1, 2, 3]) - res = array / 2 - assert res[0].scalar() == 0.5 - assert res[1].scalar() == 1 - assert res[2].scalar() == 1.5 - - res = array / 1.5 - assert round(res[0].scalar(), 5) == 0.66667 # type: ignore[arg-type] - assert round(res[1].scalar(), 5) == 1.33333 # type: ignore[arg-type] - assert res[2].scalar() == 2 - - res = array / Array([2, 2, 2]) - assert res[0].scalar() == 0.5 - assert res[1].scalar() == 1 - assert res[2].scalar() == 1.5 - - -def test_array_floordiv() -> None: - # TODO add test after implementation of __floordiv__ - pass +class TestArithmeticOperators: + def setup_method(self, method: Any) -> None: + self.list = [1, 2, 3] + self.const_int = 2 + self.const_float = 1.5 + self.array = Array(self.list) + self.array_other = Array([9, 9, 9]) + + self.tuple = (1, 2, 3) + self.const_str = "15" + + def teardown_method(self, method: Any) -> None: + self.array = Array(self.list) + + def test_add_int(self) -> None: + res = self.array + self.const_int + assert res[0].scalar() == 3 + assert res[1].scalar() == 4 + assert res[2].scalar() == 5 + + # Test __add__, __iadd__, __radd__ + + def test_add_float(self) -> None: + res = self.array + self.const_float + assert res[0].scalar() == 2.5 + assert res[1].scalar() == 3.5 + assert res[2].scalar() == 4.5 + + def test_add_array(self) -> None: + res = self.array + self.array_other + assert res[0].scalar() == 10 + assert res[1].scalar() == 11 + assert res[2].scalar() == 12 + + def test_add_inplace_and_reflected(self) -> None: + res = self.array + self.const_int + ires = self.array + ires += self.const_int + rres = self.const_int + self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == rres[0].scalar() == 3 + assert res[1].scalar() == ires[1].scalar() == rres[1].scalar() == 4 + assert res[2].scalar() == ires[2].scalar() == rres[2].scalar() == 5 + + 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_add_raises_type_error(self) -> None: + with pytest.raises(TypeError): + Array([1, 2, 3]) + self.const_str # type: ignore[operator] + with pytest.raises(TypeError): + Array([1, 2, 3]) + self.tuple # type: ignore[operator] + + # Test __sub__, __isub__, __rsub__ + + def test_sub_int(self) -> None: + res = self.array - self.const_int + assert res[0].scalar() == -1 + assert res[1].scalar() == 0 + assert res[2].scalar() == 1 + + def test_sub_float(self) -> None: + res = self.array - self.const_float + assert res[0].scalar() == -0.5 + assert res[1].scalar() == 0.5 + assert res[2].scalar() == 1.5 + + def test_sub_arr(self) -> None: + res = self.array - self.array_other + assert res[0].scalar() == -8 + assert res[1].scalar() == -7 + assert res[2].scalar() == -6 + + def test_sub_inplace_and_reflected(self) -> None: + res = self.array - self.const_int + ires = self.array + ires -= self.const_int + rres = self.const_int - self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == rres[0].scalar() == -1 + assert res[1].scalar() == ires[1].scalar() == rres[1].scalar() == 0 + assert res[2].scalar() == ires[2].scalar() == rres[2].scalar() == 1 + + 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_sub_raises_type_error(self) -> None: + with pytest.raises(TypeError): + self.array - self.const_str # type: ignore[operator] -def test_array_mod() -> None: - array = Array([1, 2, 3]) - res = array % 2 - assert res[0].scalar() == 1 - assert res[1].scalar() == 0 - assert res[2].scalar() == 1 + with pytest.raises(TypeError): + self.array - self.tuple # type: ignore[operator] + + # Test __mul__, __imul__, __rmul__ + + def test_mul_int(self) -> None: + res = self.array * self.const_int + assert res[0].scalar() == 2 + assert res[1].scalar() == 4 + assert res[2].scalar() == 6 + + def test_mul_float(self) -> None: + res = self.array * self.const_float + assert res[0].scalar() == 1.5 + assert res[1].scalar() == 3 + assert res[2].scalar() == 4.5 + + def test_mul_array(self) -> None: + res = self.array * self.array_other + assert res[0].scalar() == 9 + assert res[1].scalar() == 18 + assert res[2].scalar() == 27 + + def test_mul_inplace_and_reflected(self) -> None: + res = self.array * self.const_int + ires = self.array + ires *= self.const_int + rres = self.const_int * self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == rres[0].scalar() == 2 + assert res[1].scalar() == ires[1].scalar() == rres[1].scalar() == 4 + assert res[2].scalar() == ires[2].scalar() == rres[2].scalar() == 6 + + 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_mul_raises_type_error(self) -> None: + with pytest.raises(TypeError): + self.array * self.const_str # type: ignore[operator] - res = array % 1.5 - assert res[0].scalar() == 1.0 - assert res[1].scalar() == 0.5 - assert res[2].scalar() == 0.0 + with pytest.raises(TypeError): + self.array * self.tuple # type: ignore[operator] + + # Test __truediv__, __itruediv__, __rtruediv__ + + def test_truediv_int(self) -> None: + res = self.array / self.const_int + assert res[0].scalar() == 0.5 + assert res[1].scalar() == 1 + assert res[2].scalar() == 1.5 + + def test_truediv_float(self) -> None: + res = self.array / self.const_float + assert round(res[0].scalar(), 5) == 0.66667 # type: ignore[arg-type] + assert round(res[1].scalar(), 5) == 1.33333 # type: ignore[arg-type] + assert res[2].scalar() == 2 + + def test_truediv_array(self) -> None: + res = self.array / self.array_other + assert round(res[0].scalar(), 5) == 0.11111 # type: ignore[arg-type] + assert round(res[1].scalar(), 5) == 0.22222 # type: ignore[arg-type] + assert round(res[2].scalar(), 5) == 0.33333 # type: ignore[arg-type] + + def test_truediv_inplace_and_reflected(self) -> None: + res = self.array / self.const_int + ires = self.array + ires /= self.const_int + rres = self.const_int / self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == 0.5 + assert res[1].scalar() == ires[1].scalar() == 1 + assert res[2].scalar() == ires[2].scalar() == 1.5 + + assert rres[0].scalar() == 2 + assert rres[1].scalar() == 1 + assert round(rres[2].scalar(), 5) == 0.66667 + + 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_truediv_raises_type_error(self) -> None: + with pytest.raises(TypeError): + self.array / self.const_str # type: ignore[operator] - res = array % Array([9, 9, 9]) - assert res[0].scalar() == 1.0 - assert res[1].scalar() == 2.0 - assert res[2].scalar() == 3.0 + with pytest.raises(TypeError): + self.array / self.tuple # type: ignore[operator] + + # TODO + # Test __floordiv__, __ifloordiv__, __rfloordiv__ + + # Test __mod__, __imod__, __rmod__ + + def test_mod_int(self) -> None: + res = self.array % self.const_int + assert res[0].scalar() == 1 + assert res[1].scalar() == 0 + assert res[2].scalar() == 1 + + def test_mod_float(self) -> None: + res = self.array % self.const_float + assert res[0].scalar() == 1.0 + assert res[1].scalar() == 0.5 + assert res[2].scalar() == 0.0 + + def test_mod_array(self) -> None: + res = self.array % self.array_other + assert res[0].scalar() == 1.0 + assert res[1].scalar() == 2.0 + assert res[2].scalar() == 3.0 + + def test_mod_inplace_and_reflected(self) -> None: + res = self.array % self.const_int + ires = self.array + ires %= self.const_int + rres = self.const_int % self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == 1 + assert res[1].scalar() == ires[1].scalar() == 0 + assert res[2].scalar() == ires[2].scalar() == 1 + + assert rres[0].scalar() == 0 + assert rres[1].scalar() == 0 + assert rres[2].scalar() == 2 + + 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_mod_raises_type_error(self) -> None: + with pytest.raises(TypeError): + self.array % self.const_str # type: ignore[operator] + with pytest.raises(TypeError): + self.array % self.tuple # type: ignore[operator] + + # Test __pow__, __ipow__, __rpow__ + + def test_pow_int(self) -> None: + res = self.array ** self.const_int + assert res[0].scalar() == 1 + assert res[1].scalar() == 4 + assert res[2].scalar() == 9 + + def test_pow_float(self) -> None: + res = self.array ** self.const_float + assert res[0].scalar() == 1 + assert round(res[1].scalar(), 5) == 2.82843 # type: ignore[arg-type] + assert round(res[2].scalar(), 5) == 5.19615 # type: ignore[arg-type] + + def test_pow_array(self) -> None: + res = self.array ** self.array_other + assert res[0].scalar() == 1 + assert res[1].scalar() == 512 + assert res[2].scalar() == 19683 + + def test_pow_inplace_and_reflected(self) -> None: + res = self.array ** self.const_int + ires = self.array + ires **= self.const_int + rres = self.const_int ** self.array # type: ignore[operator] + + assert res[0].scalar() == ires[0].scalar() == 1 + assert res[1].scalar() == ires[1].scalar() == 4 + assert res[2].scalar() == ires[2].scalar() == 9 + + assert rres[0].scalar() == 2 + assert rres[1].scalar() == 4 + assert rres[2].scalar() == 8 + + 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_pow_raises_type_error(self) -> None: + with pytest.raises(TypeError): + self.array % self.const_str # type: ignore[operator] -def test_array_pow() -> None: - array = Array([1, 2, 3]) - res = array ** 2 - assert res[0].scalar() == 1 - assert res[1].scalar() == 4 - assert res[2].scalar() == 9 - - res = array ** 1.5 - assert res[0].scalar() == 1 - assert round(res[1].scalar(), 5) == 2.82843 # type: ignore[arg-type] - assert round(res[2].scalar(), 5) == 5.19615 # type: ignore[arg-type] - - res = array ** Array([9, 9, 9]) - assert res[0].scalar() == 1 - assert res[1].scalar() == 512 - assert res[2].scalar() == 19683 + with pytest.raises(TypeError): + self.array % self.tuple # type: ignore[operator] From 0afb92eeb047e0b60cbe1eeeb05cf8182f8084fe Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 29 Jan 2023 04:15:34 +0200 Subject: [PATCH 16/29] Fix reflected operators bug. Add test coverage for the rest of the arithmetic operators --- arrayfire/array_api/_array_object.py | 58 +++++++++++-------- arrayfire/array_api/_utils.py | 5 -- .../array_api/tests/test_array_object.py | 17 +++--- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index bb676c3fd..6232baca6 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -19,7 +19,7 @@ from ._dtypes import int64 as af_int64 from ._dtypes import supported_dtypes from ._dtypes import uint64 as af_uint64 -from ._utils import PointerSource, is_number, to_str +from ._utils import PointerSource, to_str ShapeType = tuple[int, ...] _bcast_var = False # HACK, TODO replace for actual bcast_var after refactoring @@ -286,25 +286,25 @@ def __radd__(self, other: Array, /) -> Array: """ Return other + self. """ - return _process_c_function(self, other, backend.get().af_add) + return _process_c_function(other, self, backend.get().af_add) def __rsub__(self, other: Array, /) -> Array: """ Return other - self. """ - return _process_c_function(self, other, backend.get().af_sub) + return _process_c_function(other, self, backend.get().af_sub) def __rmul__(self, other: Array, /) -> Array: """ Return other * self. """ - return _process_c_function(self, other, backend.get().af_mul) + return _process_c_function(other, self, backend.get().af_mul) def __rtruediv__(self, other: Array, /) -> Array: """ Return other / self. """ - return _process_c_function(self, other, backend.get().af_div) + return _process_c_function(other, self, backend.get().af_div) def __rfloordiv__(self, other: Array, /) -> Array: # TODO @@ -314,13 +314,13 @@ def __rmod__(self, other: Array, /) -> Array: """ Return other / self. """ - return _process_c_function(self, other, backend.get().af_mod) + return _process_c_function(other, self, backend.get().af_mod) def __rpow__(self, other: Array, /) -> Array: """ Return other ** self. """ - return _process_c_function(self, other, backend.get().af_pow) + return _process_c_function(other, self, backend.get().af_pow) # Reflected Array Operators @@ -334,31 +334,31 @@ def __rand__(self, other: Array, /) -> Array: """ Return other & self. """ - return _process_c_function(self, other, backend.get().af_bitand) + return _process_c_function(other, self, backend.get().af_bitand) def __ror__(self, other: Array, /) -> Array: """ Return other & self. """ - return _process_c_function(self, other, backend.get().af_bitor) + return _process_c_function(other, self, backend.get().af_bitor) def __rxor__(self, other: Array, /) -> Array: """ Return other ^ self. """ - return _process_c_function(self, other, backend.get().af_bitxor) + return _process_c_function(other, self, backend.get().af_bitxor) def __rlshift__(self, other: Array, /) -> Array: """ Return other << self. """ - return _process_c_function(self, other, backend.get().af_bitshiftl) + return _process_c_function(other, self, backend.get().af_bitshiftl) def __rrshift__(self, other: Array, /) -> Array: """ Return other >> self. """ - return _process_c_function(self, other, backend.get().af_bitshiftr) + return _process_c_function(other, self, backend.get().af_bitshiftr) # In-place Arithmetic Operators @@ -614,20 +614,32 @@ def _str_to_dtype(value: int) -> Dtype: def _process_c_function( - target: Array, other: int | float | bool | complex | Array, c_function: Any) -> Array: + lhs: int | float | bool | complex | Array, rhs: int | float | bool | complex | Array, + c_function: Any) -> Array: out = Array() - # TODO discuss the difference between binary_func and binary_funcr - # because implementation looks like exectly the same. - # consider chaging to __iadd__ = __radd__ = __add__ interfce if no difference - if isinstance(other, Array): - safe_call(c_function(ctypes.pointer(out.arr), target.arr, other.arr, _bcast_var)) - elif is_number(other): - other_dtype = _implicit_dtype(other, target.dtype) - other_array = _constant_array(other, CShape(*target.shape), other_dtype) - safe_call(c_function(ctypes.pointer(out.arr), target.arr, other_array.arr, _bcast_var)) + 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 | bool | complex): + rhs_dtype = _implicit_dtype(rhs, lhs.dtype) + rhs_constant_array = _constant_array(rhs, CShape(*lhs.shape), rhs_dtype) + + lhs_array = lhs.arr + rhs_array = rhs_constant_array.arr + + elif isinstance(lhs, int | float | bool | complex) and isinstance(rhs, Array): + lhs_dtype = _implicit_dtype(lhs, rhs.dtype) + lhs_constant_array = _constant_array(lhs, CShape(*rhs.shape), lhs_dtype) + + lhs_array = lhs_constant_array.arr + rhs_array = rhs.arr + else: - raise TypeError(f"{type(other)} is not supported and can not be passed to C binary function.") + raise TypeError(f"{type(rhs)} is not supported and can not be passed to C binary function.") + + safe_call(c_function(ctypes.pointer(out.arr), lhs_array, rhs_array, _bcast_var)) return out diff --git a/arrayfire/array_api/_utils.py b/arrayfire/array_api/_utils.py index 67b06f253..15a8a2564 100644 --- a/arrayfire/array_api/_utils.py +++ b/arrayfire/array_api/_utils.py @@ -1,6 +1,5 @@ import ctypes import enum -import numbers class PointerSource(enum.Enum): @@ -14,7 +13,3 @@ class PointerSource(enum.Enum): def to_str(c_str: ctypes.c_char_p) -> str: return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] - - -def is_number(number: int | float | bool | complex) -> bool: - return isinstance(number, numbers.Number) diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 4f50019ce..44d033fd8 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -179,9 +179,6 @@ def setup_method(self, method: Any) -> None: self.tuple = (1, 2, 3) self.const_str = "15" - def teardown_method(self, method: Any) -> None: - self.array = Array(self.list) - def test_add_int(self) -> None: res = self.array + self.const_int assert res[0].scalar() == 3 @@ -220,10 +217,10 @@ def test_add_inplace_and_reflected(self) -> None: def test_add_raises_type_error(self) -> None: with pytest.raises(TypeError): - Array([1, 2, 3]) + self.const_str # type: ignore[operator] + self.array + self.const_str # type: ignore[operator] with pytest.raises(TypeError): - Array([1, 2, 3]) + self.tuple # type: ignore[operator] + self.array + self.tuple # type: ignore[operator] # Test __sub__, __isub__, __rsub__ @@ -251,9 +248,13 @@ def test_sub_inplace_and_reflected(self) -> None: ires -= self.const_int rres = self.const_int - self.array # type: ignore[operator] - assert res[0].scalar() == ires[0].scalar() == rres[0].scalar() == -1 - assert res[1].scalar() == ires[1].scalar() == rres[1].scalar() == 0 - assert res[2].scalar() == ires[2].scalar() == rres[2].scalar() == 1 + assert res[0].scalar() == ires[0].scalar() == -1 + assert res[1].scalar() == ires[1].scalar() == 0 + assert res[2].scalar() == ires[2].scalar() == 1 + + assert rres[0].scalar() == 1 + assert rres[1].scalar() == 0 + assert rres[2].scalar() == -1 assert res.dtype == ires.dtype == rres.dtype assert res.ndim == ires.ndim == rres.ndim From 1d071be2c8059f36fb656dbc1b16f72c8ad6212d Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 17:11:23 +0200 Subject: [PATCH 17/29] Add required by specification methods --- arrayfire/array_api/_array_object.py | 95 ++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/arrayfire/array_api/_array_object.py b/arrayfire/array_api/_array_object.py index 6232baca6..41fa9c6f0 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/_array_object.py @@ -2,6 +2,7 @@ import array as py_array import ctypes +import enum from dataclasses import dataclass from typing import Any @@ -125,21 +126,6 @@ def __init__( ctypes.pointer(_cshape.c_array), ctypes.pointer(strides_cshape), dtype.c_api_value, pointer_source.value)) - 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) + _array_as_str(self) - - def __repr__(self) -> str: - # return _metadata_string(self.dtype, self.shape) - # TODO change the look of array representation. E.g., like np.array - return _array_as_str(self) - - def __len__(self) -> int: - return self.shape[0] if self.shape else 0 - # Arithmetic Operators def __pos__(self) -> Array: @@ -441,6 +427,36 @@ def __irshift__(self, other: int | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_bitshiftr) + # Methods + + def __abs__(self) -> Array: + # TODO + return NotImplemented + + def __array_namespace__(self, *, api_version: None | str = None) -> Any: + # TODO + return NotImplemented + + def __bool__(self) -> bool: + # TODO + return NotImplemented + + def __complex__(self) -> complex: + # TODO + return NotImplemented + + def __dlpack__(self, *, stream: 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: int | slice | tuple[int | slice] | Array, /) -> Array: # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array # TODO: refactor @@ -456,6 +472,40 @@ def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array ctypes.pointer(out.arr), self.arr, c_dim_t(ndims), _get_indices(key).pointer)) 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: int | slice | tuple[int | slice, ...] | Array, value: 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) + _array_as_str(self) + + def __repr__(self) -> str: + # return _metadata_string(self.dtype, self.shape) + # TODO change the look of array representation. E.g., like np.array + return _array_as_str(self) + + def to_device(self, device: Any, /, *, stream: None | int | Any = None) -> Array: + # TODO implementation and change device type from Any to Device + return NotImplemented + + # Attributes + @property def dtype(self) -> Dtype: out = ctypes.c_int() @@ -464,17 +514,23 @@ def dtype(self) -> Dtype: @property def device(self) -> Any: - raise NotImplementedError + # TODO + return NotImplemented @property def mT(self) -> Array: # TODO - raise NotImplementedError + return NotImplemented @property def T(self) -> Array: - # TODO - raise NotImplementedError + """ + Transpose of the array. + """ + out = Array() + # NOTE conj support is removed because it is never used + safe_call(backend.get().af_transpose(ctypes.pointer(out.arr), self.arr, False)) + return out @property def size(self) -> int: @@ -507,6 +563,7 @@ def scalar(self) -> 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 From 04fbb1b27bf1cd7452768b082ee08b1b8e9ef825 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 17:23:24 +0200 Subject: [PATCH 18/29] Change file names --- arrayfire/array_api/__init__.py | 4 ++-- .../{_array_object.py => array_object.py} | 22 +++++++++---------- arrayfire/array_api/{_dtypes.py => dtypes.py} | 0 .../array_api/tests/test_array_object.py | 4 ++-- arrayfire/array_api/{_utils.py => utils.py} | 0 5 files changed, 15 insertions(+), 15 deletions(-) rename arrayfire/array_api/{_array_object.py => array_object.py} (98%) rename arrayfire/array_api/{_dtypes.py => dtypes.py} (100%) rename arrayfire/array_api/{_utils.py => utils.py} (100%) diff --git a/arrayfire/array_api/__init__.py b/arrayfire/array_api/__init__.py index 2de1832ab..675b27ade 100644 --- a/arrayfire/array_api/__init__.py +++ b/arrayfire/array_api/__init__.py @@ -5,5 +5,5 @@ "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 +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 similarity index 98% rename from arrayfire/array_api/_array_object.py rename to arrayfire/array_api/array_object.py index 41fa9c6f0..118fe1d6e 100644 --- a/arrayfire/array_api/_array_object.py +++ b/arrayfire/array_api/array_object.py @@ -10,17 +10,17 @@ from arrayfire.algorithm import count # TODO refactor from arrayfire.array import _get_indices, _in_display_dims_limit # TODO refactor -from ._dtypes import CShape, Dtype -from ._dtypes import bool as af_bool -from ._dtypes import c_dim_t -from ._dtypes import complex64 as af_complex64 -from ._dtypes import complex128 as af_complex128 -from ._dtypes import float32 as af_float32 -from ._dtypes import float64 as af_float64 -from ._dtypes import int64 as af_int64 -from ._dtypes import supported_dtypes -from ._dtypes import uint64 as af_uint64 -from ._utils import PointerSource, to_str +from .dtypes import CShape, Dtype +from .dtypes import bool as af_bool +from .dtypes import c_dim_t +from .dtypes import complex64 as af_complex64 +from .dtypes import complex128 as af_complex128 +from .dtypes import float32 as af_float32 +from .dtypes import float64 as af_float64 +from .dtypes import int64 as af_int64 +from .dtypes import supported_dtypes +from .dtypes import uint64 as af_uint64 +from .utils import PointerSource, to_str ShapeType = tuple[int, ...] _bcast_var = False # HACK, TODO replace for actual bcast_var after refactoring diff --git a/arrayfire/array_api/_dtypes.py b/arrayfire/array_api/dtypes.py similarity index 100% rename from arrayfire/array_api/_dtypes.py rename to arrayfire/array_api/dtypes.py diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 44d033fd8..579076495 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -3,8 +3,8 @@ import pytest -from arrayfire.array_api import Array, float32, int16 -from arrayfire.array_api._dtypes import supported_dtypes +from arrayfire.array_api.array_object import Array +from arrayfire.array_api.dtypes import float32, int16, supported_dtypes # TODO change separated methods with setup and teardown to avoid code duplication # TODO add tests for array arguments: device, offset, strides diff --git a/arrayfire/array_api/_utils.py b/arrayfire/array_api/utils.py similarity index 100% rename from arrayfire/array_api/_utils.py rename to arrayfire/array_api/utils.py From 2d91b042d64de3797e551ea8902b9716c1cf29c1 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 18:13:54 +0200 Subject: [PATCH 19/29] Change utils. Add docstrings --- arrayfire/array_api/array_object.py | 8 +- arrayfire/array_api/device.py | 10 ++ arrayfire/array_api/dtype_functions.py | 138 +++++++++++++++++++++++++ arrayfire/array_api/utils.py | 82 +++++++++++++-- 4 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 arrayfire/array_api/device.py create mode 100644 arrayfire/array_api/dtype_functions.py diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 118fe1d6e..2b09b69c6 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -10,6 +10,7 @@ from arrayfire.algorithm import count # TODO refactor from arrayfire.array import _get_indices, _in_display_dims_limit # TODO refactor +from .device import PointerSource from .dtypes import CShape, Dtype from .dtypes import bool as af_bool from .dtypes import c_dim_t @@ -20,7 +21,6 @@ from .dtypes import int64 as af_int64 from .dtypes import supported_dtypes from .dtypes import uint64 as af_uint64 -from .utils import PointerSource, to_str ShapeType = tuple[int, ...] _bcast_var = False # HACK, TODO replace for actual bcast_var after refactoring @@ -632,7 +632,7 @@ def _array_as_str(array: Array) -> str: arr_str = ctypes.c_char_p(0) # FIXME add description to passed arguments safe_call(backend.get().af_array_to_string(ctypes.pointer(arr_str), "", array.arr, 4, True)) - py_str = to_str(arr_str) + py_str = _to_str(arr_str) safe_call(backend.get().af_free_host(arr_str)) return py_str @@ -662,6 +662,10 @@ def _c_api_value_to_dtype(value: int) -> Dtype: raise TypeError("There is no supported dtype that matches passed dtype C API value.") +def _to_str(c_str: ctypes.c_char_p) -> str: + return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] + + def _str_to_dtype(value: int) -> Dtype: for dtype in supported_dtypes: if value == dtype.typecode or value == dtype.typename: diff --git a/arrayfire/array_api/device.py b/arrayfire/array_api/device.py new file mode 100644 index 000000000..fde5d6a54 --- /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/dtype_functions.py b/arrayfire/array_api/dtype_functions.py new file mode 100644 index 000000000..c0a8469c5 --- /dev/null +++ b/arrayfire/array_api/dtype_functions.py @@ -0,0 +1,138 @@ +from .array_object import Array +from .dtypes import Dtype + +# TODO implement functions + + +def astype(x: Array, dtype: Dtype, /, *, copy: bool = True) -> Array: + """ + Copies an array to a specified data type irrespective of Type Promotion Rules rules. + + Parameters + ---------- + x : Array + Array to cast. + dtype: Dtype + Desired data type. + copy: bool, optional + Specifies whether to copy an array when the specified dtype matches the data type of the input array x. + If True, a newly allocated array must always be returned. If False and the specified dtype matches the data + type of the input array, the input array must be returned; otherwise, a newly allocated array must be returned. + Default: True. + + Returns + ------- + out : Array + An array having the specified data type. The returned array must have the same shape as x. + + Note + ---- + - Casting floating-point NaN and infinity values to integral data types is not specified and is + implementation-dependent. + - Casting a complex floating-point array to a real-valued data type should not be permitted. + Historically, when casting a complex floating-point array to a real-valued data type, libraries such as NumPy have + discarded imaginary components such that, for a complex floating-point array x, astype(x) equals astype(real(x))). + This behavior is considered problematic as the choice to discard the imaginary component is arbitrary and + introduces more than one way to achieve the same outcome (i.e., for a complex floating-point array x, astype(x) and + astype(real(x)) versus only astype(imag(x))). Instead, in order to avoid ambiguity and to promote clarity, this + specification requires that array API consumers explicitly express which component should be cast to a specified + real-valued data type. + - When casting a boolean input array to a real-valued data type, a value of True must cast to a real-valued number + equal to 1, and a value of False must cast to a real-valued number equal to 0. + When casting a boolean input array to a complex floating-point data type, a value of True must cast to a complex + number equal to 1 + 0j, and a value of False must cast to a complex number equal to 0 + 0j. + - When casting a real-valued input array to bool, a value of 0 must cast to False, and a non-zero value must cast + to True. + When casting a complex floating-point array to bool, a value of 0 + 0j must cast to False, and all other values + must cast to True. + """ + return NotImplemented + + +def can_cast(from_: Dtype | Array, to: Dtype, /) -> bool: + """ + Determines if one data type can be cast to another data type according Type Promotion Rules rules. + + Parameters + ---------- + from_ : Dtype | Array + Input data type or array from which to cast. + to : Dtype + Desired data type. + + Returns + ------- + out : bool + True if the cast can occur according to Type Promotion Rules rules; otherwise, False. + """ + return NotImplemented + + +def finfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] + # TODO add docstring, implementation and return type -> finfo_object + return NotImplemented + + +def iinfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] + # TODO add docstring, implementation and return type -> iinfo_object + return NotImplemented + + +def isdtype(dtype: Dtype, kind: Dtype | str | tuple[Dtype | str, ...]) -> bool: + """ + Returns a boolean indicating whether a provided dtype is of a specified data type “kind”. + + Parameters + ---------- + dtype : Dtype + The input dtype. + kind : Dtype | str | tuple[Dtype | str, ...] + Data type kind. + - If kind is a dtype, the function must return a boolean indicating whether the input dtype is equal to the + dtype specified by kind. + - If kind is a string, the function must return a boolean indicating whether the input dtype is of a specified + data type kind. The following dtype kinds must be supported: + - bool: boolean data types (e.g., bool). + - signed integer: signed integer data types (e.g., int8, int16, int32, int64). + - unsigned integer: unsigned integer data types (e.g., uint8, uint16, uint32, uint64). + - integral: integer data types. Shorthand for ('signed integer', 'unsigned integer'). + - real floating: real-valued floating-point data types (e.g., float32, float64). + - complex floating: complex floating-point data types (e.g., complex64, complex128). + - numeric: numeric data types. Shorthand for ('integral', 'real floating', 'complex floating'). + - If kind is a tuple, the tuple specifies a union of dtypes and/or kinds, and the function must return a + boolean indicating whether the input dtype is either equal to a specified dtype or belongs to at least one + specified data type kind. + + Returns + ------- + out : bool + Boolean indicating whether a provided dtype is of a specified data type kind. + + Note + ---- + - A conforming implementation of the array API standard is not limited to only including the dtypes described in + this specification in the required data type kinds. For example, implementations supporting float16 and bfloat16 + can include float16 and bfloat16 in the real floating data type kind. Similarly, implementations supporting int128 + can include int128 in the signed integer data type kind. + In short, conforming implementations may extend data type kinds; however, data type kinds must remain consistent + (e.g., only integer dtypes may belong to integer data type kinds and only floating-point dtypes may belong to + floating-point data type kinds), and extensions must be clearly documented as such in library documentation. + """ + return NotImplemented + + +def result_type(*arrays_and_dtypes: Dtype | Array) -> Dtype: + """ + Returns the dtype that results from applying the type promotion rules (see Type Promotion Rules) to the arguments. + + Parameters + ---------- + arrays_and_dtypes: Dtype | Array + An arbitrary number of input arrays and/or dtypes. + + Returns + ------- + out : Dtype + The dtype resulting from an operation involving the input arrays and dtypes. + """ + return NotImplemented diff --git a/arrayfire/array_api/utils.py b/arrayfire/array_api/utils.py index 15a8a2564..f93ab9ea8 100644 --- a/arrayfire/array_api/utils.py +++ b/arrayfire/array_api/utils.py @@ -1,15 +1,79 @@ -import ctypes -import enum +from .array_object import Array +# TODO implement functions -class PointerSource(enum.Enum): + +def all(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: """ - Source of the pointer + Tests whether all input array elements evaluate to True along a specified axis. + + Parameters + ---------- + x : Array + Input array. + axis : None | int | tuple[int, ...], optional + Axis or axes along which to perform a logical AND reduction. By default, a logical AND reduction must be + performed over the entire array. If a tuple of integers, logical AND reductions must be performed over + multiple axes. A valid axis must be an integer on the interval [-N, N), where N is the rank (number of + dimensions) of x. If an axis is specified as a negative integer, the function must determine the axis along + which to perform a reduction by counting backward from the last dimension (where -1 refers to the last + dimension). If provided an invalid axis, the function must raise an exception. Default: None. + keepdims : bool, optional + If True, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, + accordingly, the result must be compatible with the input array (see Broadcasting). Otherwise, if False, + the reduced axes (dimensions) must not be included in the result. Default: False. + + Returns + ------- + out : Array + If a logical AND reduction was performed over the entire array, the returned array must be a zero-dimensional + array containing the test result; otherwise, the returned array must be a non-zero-dimensional array + containing the test results. The returned array must have a data type of bool. + + Note + ---- + - Positive infinity, negative infinity, and NaN must evaluate to True. + - If x has a complex floating-point data type, elements having a non-zero component (real or imaginary) must + evaluate to True. + - If x is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test + result must be True. """ - # FIXME - device = 0 - host = 1 + return NotImplemented -def to_str(c_str: ctypes.c_char_p) -> str: - return str(c_str.value.decode("utf-8")) # type: ignore[union-attr] +def any(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: + """ + Tests whether any input array element evaluates to True along a specified axis. + + Parameters + ---------- + x : Array + Input array. + axis : None | int | tuple[int, ...], optional + Axis or axes along which to perform a logical OR reduction. By default, a logical OR reduction must be + performed over the entire array. If a tuple of integers, logical OR reductions must be performed over + multiple axes. A valid axis must be an integer on the interval [-N, N), where N is the rank (number of + dimensions) of x. If an axis is specified as a negative integer, the function must determine the axis along + which to perform a reduction by counting backward from the last dimension (where -1 refers to the last + dimension). If provided an invalid axis, the function must raise an exception. Default: None. + keepdims : bool, optional + If True, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, + accordingly, the result must be compatible with the input array (see Broadcasting). Otherwise, if False, + the reduced axes (dimensions) must not be included in the result. Default: False. + + Returns + ------- + out : Array + If a logical OR reduction was performed over the entire array, the returned array must be a zero-dimensional + array containing the test result; otherwise, the returned array must be a non-zero-dimensional array + containing the test results. The returned array must have a data type of bool. + + Note + ---- + - Positive infinity, negative infinity, and NaN must evaluate to True. + - If x has a complex floating-point data type, elements having a non-zero component (real or imaginary) must + evaluate to True. + - If x is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test + result must be False. + """ + return NotImplemented From 59393880a41221f65129cc3eeaaca966bff52142 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 18:52:04 +0200 Subject: [PATCH 20/29] Add docstrings for operators --- arrayfire/array_api/array_object.py | 238 ++++++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 14 deletions(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 2b09b69c6..7ecc7a355 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -130,38 +130,124 @@ def __init__( def __pos__(self) -> Array: """ - Return +self + 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: """ - Return -self + 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 0 - self # type: ignore[no-any-return, operator] # FIXME def __add__(self, other: int | float | Array, /) -> Array: # TODO discuss either we need to support complex and bool as other input type """ - Return self + other. + 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: 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.get().af_add) def __sub__(self, other: int | float | Array, /) -> Array: """ - Return self - other. + 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: 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.get().af_sub) def __mul__(self, other: int | float | Array, /) -> Array: """ - Return self * other. + 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: 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.get().af_mul) def __truediv__(self, other: int | float | Array, /) -> Array: """ - Return self / other. + 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: 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.get().af_div) @@ -171,13 +257,57 @@ def __floordiv__(self, other: int | float | Array, /) -> Array: def __mod__(self, other: int | float | Array, /) -> Array: """ - Return self % other. + 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: 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.get().af_mod) def __pow__(self, other: int | float | Array, /) -> Array: """ - Return self ** other. + 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: 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. + + Note + ---- + - If both self and other have integer data types, the result of __pow__ when other_i is negative + (i.e., less than zero) is unspecified and thus implementation-dependent. + If self has an integer data type and other has a floating-point data type, behavior is + implementation-dependent, as type promotion between data type “kinds” (e.g., integer versus floating-point) + is unspecified. """ return _process_c_function(self, other, backend.get().af_pow) @@ -191,7 +321,17 @@ def __matmul__(self, other: Array, /) -> Array: def __invert__(self) -> Array: """ - Return ~self. + 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. """ out = Array() safe_call(backend.get().af_bitnot(ctypes.pointer(out.arr), self.arr)) @@ -199,31 +339,101 @@ def __invert__(self) -> Array: def __and__(self, other: int | bool | Array, /) -> Array: """ - Return self & other. + 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: 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 must have a data type determined + by Type Promotion Rules. """ return _process_c_function(self, other, backend.get().af_bitand) def __or__(self, other: int | bool | Array, /) -> Array: """ - Return self | other. + 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: 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 must have a data type determined + by Type Promotion Rules. """ return _process_c_function(self, other, backend.get().af_bitor) def __xor__(self, other: int | bool | Array, /) -> Array: """ - Return self ^ other. + 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: 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 must have a data type determined + by Type Promotion Rules. """ return _process_c_function(self, other, backend.get().af_bitxor) def __lshift__(self, other: int | Array, /) -> Array: """ - Return self << other. + 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: int | float | 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.get().af_bitshiftl) def __rshift__(self, other: int | Array, /) -> Array: """ - Return self >> other. + 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: int | float | 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.get().af_bitshiftr) From 0231e2774002c093f58fd381b533455953408a60 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 19:46:42 +0200 Subject: [PATCH 21/29] Change TODOs --- .github/workflows/build.yaml | 3 ++ arrayfire/array_api/array_object.py | 30 ++++++------------- .../array_api/tests/test_array_object.py | 1 + 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8e420cfbc..c3ca58a39 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,5 +22,8 @@ jobs: - 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/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 7ecc7a355..7512c88d3 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -23,7 +23,10 @@ from .dtypes import uint64 as af_uint64 ShapeType = tuple[int, ...] -_bcast_var = False # HACK, TODO replace for actual bcast_var after refactoring +# HACK, TODO replace for actual bcast_var after refactoring ~ https://github.com/arrayfire/arrayfire/pull/2871 +_bcast_var = False + +# TODO use int | float in operators -> remove bool | complex support @dataclass @@ -33,12 +36,6 @@ class _ArrayBuffer: class Array: - # Numpy checks this attribute to know which class handles binary builtin operations, such as __add__. - # Setting to such a high value should make sure that arrayfire has priority over - # other classes, ensuring that e.g. numpy.float32(1)*arrayfire.randu(3) is handled by - # arrayfire's __radd__() instead of numpy's __add__() - __array_priority__ = 30 # TODO discuss its purpose - def __init__( self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, dtype: None | Dtype | str = None, shape: None | ShapeType = None, @@ -164,7 +161,6 @@ def __neg__(self) -> Array: return 0 - self # type: ignore[no-any-return, operator] # FIXME def __add__(self, other: int | float | Array, /) -> Array: - # TODO discuss either we need to support complex and bool as other input type """ Calculates the sum for each element of an array instance with the respective element of the array other. @@ -300,21 +296,13 @@ def __pow__(self, other: int | float | Array, /) -> Array: out : Array An array containing the element-wise results. The returned array must have a data type determined by Type Promotion Rules. - - Note - ---- - - If both self and other have integer data types, the result of __pow__ when other_i is negative - (i.e., less than zero) is unspecified and thus implementation-dependent. - If self has an integer data type and other has a floating-point data type, behavior is - implementation-dependent, as type promotion between data type “kinds” (e.g., integer versus floating-point) - is unspecified. """ return _process_c_function(self, other, backend.get().af_pow) # Array Operators def __matmul__(self, other: Array, /) -> Array: - # TODO + # TODO get from blas - make vanilla version and not copy af.matmul as is return NotImplemented # Bitwise Operators @@ -508,7 +496,7 @@ def __rfloordiv__(self, other: Array, /) -> Array: def __rmod__(self, other: Array, /) -> Array: """ - Return other / self. + Return other % self. """ return _process_c_function(other, self, backend.get().af_mod) @@ -534,7 +522,7 @@ def __rand__(self, other: Array, /) -> Array: def __ror__(self, other: Array, /) -> Array: """ - Return other & self. + Return other | self. """ return _process_c_function(other, self, backend.get().af_bitor) @@ -648,7 +636,7 @@ def __array_namespace__(self, *, api_version: None | str = None) -> Any: return NotImplemented def __bool__(self) -> bool: - # TODO + # TODO consider using scalar() and is_scalar() return NotImplemented def __complex__(self) -> complex: @@ -668,7 +656,7 @@ def __float__(self) -> float: return NotImplemented def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: - # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array + # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array - consider using af.span # TODO: refactor out = Array() ndims = self.ndim diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 579076495..317390929 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -9,6 +9,7 @@ # TODO change separated methods with setup and teardown to avoid code duplication # TODO add tests for array arguments: device, offset, strides # TODO add tests for all supported dtypes on initialisation +# TODO check if e.g. abs(x1-x2) < 1e-6 ~ https://davidamos.dev/the-right-way-to-compare-floats-in-python/ def test_create_empty_array() -> None: From 07c42060aeba5fc0a7bb60e6ea0d53665ede92c8 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 30 Jan 2023 20:53:12 +0200 Subject: [PATCH 22/29] Add docstrings for other operators. Remove docstrings from mocks --- arrayfire/array_api/array_object.py | 170 ++++++++++++++++-- arrayfire/array_api/dtype_functions.py | 112 +----------- .../array_api/tests/test_array_object.py | 9 +- arrayfire/array_api/utils.py | 68 ------- 4 files changed, 161 insertions(+), 198 deletions(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 7512c88d3..0df29c267 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -334,7 +334,7 @@ def __and__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: int | bool | Array Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -354,7 +354,7 @@ def __or__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: int | bool | Array Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -374,7 +374,7 @@ def __xor__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: int | bool | Array Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -394,7 +394,7 @@ def __lshift__(self, other: int | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: 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. @@ -414,7 +414,7 @@ def __rshift__(self, other: int | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: 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. @@ -429,44 +429,121 @@ def __rshift__(self, other: int | Array, /) -> Array: def __lt__(self, other: int | float | Array, /) -> Array: """ - Return self < other. + 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: 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.get().af_lt) def __le__(self, other: int | float | Array, /) -> Array: """ - Return self <= other. + 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: 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.get().af_le) def __gt__(self, other: int | float | Array, /) -> Array: """ - Return self > other. + 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: 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.get().af_gt) def __ge__(self, other: int | float | Array, /) -> Array: """ - Return self >= other. + 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: 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.get().af_ge) def __eq__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME """ - Return self == other. + 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: 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.get().af_eq) def __ne__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME """ - Return self != other. + 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: 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.get().af_neq) # Reflected Arithmetic Operators def __radd__(self, other: Array, /) -> Array: - # TODO discuss either we need to support complex and bool as other input type """ Return other + self. """ @@ -656,8 +733,24 @@ def __float__(self) -> float: return NotImplemented def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: - # TODO: API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array - consider using af.span - # TODO: refactor + """ + Returns self[key]. + + Parameters + ---------- + self : Array + Array instance. + key : int | slice | tuple[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: int | slice | ellipsis | tuple[int | slice] | Array. + # consider using af.span to replace ellipsis during refactoring out = Array() ndims = self.ndim @@ -706,6 +799,14 @@ def to_device(self, device: Any, /, *, stream: None | int | Any = None) -> Array @property def dtype(self) -> Dtype: + """ + Data type of the array elements. + + Returns + ------- + out : Dtype + Array data type. + """ out = ctypes.c_int() safe_call(backend.get().af_get_type(ctypes.pointer(out), self.arr)) return _c_api_value_to_dtype(out.value) @@ -724,14 +825,40 @@ def mT(self) -> Array: 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() - # NOTE conj support is removed because it is never used safe_call(backend.get().af_transpose(ctypes.pointer(out.arr), 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() out = c_dim_t(0) safe_call(backend.get().af_get_elements(ctypes.pointer(out), self.arr)) @@ -739,6 +866,12 @@ def size(self) -> int: @property def ndim(self) -> int: + """ + Number of array dimensions (axes). + + out : int + Number of array dimensions (axes). + """ out = ctypes.c_uint(0) safe_call(backend.get().af_get_numdims(ctypes.pointer(out), self.arr)) return out.value @@ -746,7 +879,12 @@ def ndim(self) -> int: @property def shape(self) -> ShapeType: """ - Return the shape of the array as a tuple. + Array dimensions. + + Returns + ------- + out : tuple[int, ...] + Array dimensions. """ # TODO refactor d0 = c_dim_t(0) diff --git a/arrayfire/array_api/dtype_functions.py b/arrayfire/array_api/dtype_functions.py index c0a8469c5..905d1ba97 100644 --- a/arrayfire/array_api/dtype_functions.py +++ b/arrayfire/array_api/dtype_functions.py @@ -5,134 +5,26 @@ def astype(x: Array, dtype: Dtype, /, *, copy: bool = True) -> Array: - """ - Copies an array to a specified data type irrespective of Type Promotion Rules rules. - - Parameters - ---------- - x : Array - Array to cast. - dtype: Dtype - Desired data type. - copy: bool, optional - Specifies whether to copy an array when the specified dtype matches the data type of the input array x. - If True, a newly allocated array must always be returned. If False and the specified dtype matches the data - type of the input array, the input array must be returned; otherwise, a newly allocated array must be returned. - Default: True. - - Returns - ------- - out : Array - An array having the specified data type. The returned array must have the same shape as x. - - Note - ---- - - Casting floating-point NaN and infinity values to integral data types is not specified and is - implementation-dependent. - - Casting a complex floating-point array to a real-valued data type should not be permitted. - Historically, when casting a complex floating-point array to a real-valued data type, libraries such as NumPy have - discarded imaginary components such that, for a complex floating-point array x, astype(x) equals astype(real(x))). - This behavior is considered problematic as the choice to discard the imaginary component is arbitrary and - introduces more than one way to achieve the same outcome (i.e., for a complex floating-point array x, astype(x) and - astype(real(x)) versus only astype(imag(x))). Instead, in order to avoid ambiguity and to promote clarity, this - specification requires that array API consumers explicitly express which component should be cast to a specified - real-valued data type. - - When casting a boolean input array to a real-valued data type, a value of True must cast to a real-valued number - equal to 1, and a value of False must cast to a real-valued number equal to 0. - When casting a boolean input array to a complex floating-point data type, a value of True must cast to a complex - number equal to 1 + 0j, and a value of False must cast to a complex number equal to 0 + 0j. - - When casting a real-valued input array to bool, a value of 0 must cast to False, and a non-zero value must cast - to True. - When casting a complex floating-point array to bool, a value of 0 + 0j must cast to False, and all other values - must cast to True. - """ return NotImplemented def can_cast(from_: Dtype | Array, to: Dtype, /) -> bool: - """ - Determines if one data type can be cast to another data type according Type Promotion Rules rules. - - Parameters - ---------- - from_ : Dtype | Array - Input data type or array from which to cast. - to : Dtype - Desired data type. - - Returns - ------- - out : bool - True if the cast can occur according to Type Promotion Rules rules; otherwise, False. - """ return NotImplemented def finfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] - # TODO add docstring, implementation and return type -> finfo_object + # NOTE expected return type -> finfo_object return NotImplemented def iinfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] - # TODO add docstring, implementation and return type -> iinfo_object + # NOTE expected return type -> iinfo_object return NotImplemented def isdtype(dtype: Dtype, kind: Dtype | str | tuple[Dtype | str, ...]) -> bool: - """ - Returns a boolean indicating whether a provided dtype is of a specified data type “kind”. - - Parameters - ---------- - dtype : Dtype - The input dtype. - kind : Dtype | str | tuple[Dtype | str, ...] - Data type kind. - - If kind is a dtype, the function must return a boolean indicating whether the input dtype is equal to the - dtype specified by kind. - - If kind is a string, the function must return a boolean indicating whether the input dtype is of a specified - data type kind. The following dtype kinds must be supported: - - bool: boolean data types (e.g., bool). - - signed integer: signed integer data types (e.g., int8, int16, int32, int64). - - unsigned integer: unsigned integer data types (e.g., uint8, uint16, uint32, uint64). - - integral: integer data types. Shorthand for ('signed integer', 'unsigned integer'). - - real floating: real-valued floating-point data types (e.g., float32, float64). - - complex floating: complex floating-point data types (e.g., complex64, complex128). - - numeric: numeric data types. Shorthand for ('integral', 'real floating', 'complex floating'). - - If kind is a tuple, the tuple specifies a union of dtypes and/or kinds, and the function must return a - boolean indicating whether the input dtype is either equal to a specified dtype or belongs to at least one - specified data type kind. - - Returns - ------- - out : bool - Boolean indicating whether a provided dtype is of a specified data type kind. - - Note - ---- - - A conforming implementation of the array API standard is not limited to only including the dtypes described in - this specification in the required data type kinds. For example, implementations supporting float16 and bfloat16 - can include float16 and bfloat16 in the real floating data type kind. Similarly, implementations supporting int128 - can include int128 in the signed integer data type kind. - In short, conforming implementations may extend data type kinds; however, data type kinds must remain consistent - (e.g., only integer dtypes may belong to integer data type kinds and only floating-point dtypes may belong to - floating-point data type kinds), and extensions must be clearly documented as such in library documentation. - """ return NotImplemented def result_type(*arrays_and_dtypes: Dtype | Array) -> Dtype: - """ - Returns the dtype that results from applying the type promotion rules (see Type Promotion Rules) to the arguments. - - Parameters - ---------- - arrays_and_dtypes: Dtype | Array - An arbitrary number of input arrays and/or dtypes. - - Returns - ------- - out : Dtype - The dtype resulting from an operation involving the input arrays and dtypes. - """ return NotImplemented diff --git a/arrayfire/array_api/tests/test_array_object.py b/arrayfire/array_api/tests/test_array_object.py index 317390929..f30567553 100644 --- a/arrayfire/array_api/tests/test_array_object.py +++ b/arrayfire/array_api/tests/test_array_object.py @@ -1,4 +1,5 @@ import array as pyarray +import math from typing import Any import pytest @@ -62,7 +63,7 @@ def test_create_empty_array_with_nonempty_shape() -> None: assert array.dtype == float32 assert array.ndim == 2 - assert array.size == 6 + assert array.size == math.prod(array.shape) == 6 assert array.shape == (2, 3) assert len(array) == 2 @@ -72,7 +73,7 @@ def test_create_array_from_1d_list() -> None: assert array.dtype == float32 assert array.ndim == 1 - assert array.size == 3 + assert array.size == math.prod(array.shape) == 3 assert array.shape == (3,) assert len(array) == 3 @@ -88,7 +89,7 @@ def test_create_array_from_pyarray() -> None: assert array.dtype == float32 assert array.ndim == 1 - assert array.size == 3 + assert array.size == math.prod(array.shape) == 3 assert array.shape == (3,) assert len(array) == 3 @@ -107,7 +108,7 @@ def test_array_from_af_array() -> None: assert array1.dtype == array2.dtype == float32 assert array1.ndim == array2.ndim == 1 - assert array1.size == array2.size == 1 + assert array1.size == array2.size == math.prod(array1.shape) == math.prod(array2.shape) == 1 assert array1.shape == array2.shape == (1,) assert len(array1) == len(array2) == 1 diff --git a/arrayfire/array_api/utils.py b/arrayfire/array_api/utils.py index f93ab9ea8..779459efb 100644 --- a/arrayfire/array_api/utils.py +++ b/arrayfire/array_api/utils.py @@ -4,76 +4,8 @@ def all(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: - """ - Tests whether all input array elements evaluate to True along a specified axis. - - Parameters - ---------- - x : Array - Input array. - axis : None | int | tuple[int, ...], optional - Axis or axes along which to perform a logical AND reduction. By default, a logical AND reduction must be - performed over the entire array. If a tuple of integers, logical AND reductions must be performed over - multiple axes. A valid axis must be an integer on the interval [-N, N), where N is the rank (number of - dimensions) of x. If an axis is specified as a negative integer, the function must determine the axis along - which to perform a reduction by counting backward from the last dimension (where -1 refers to the last - dimension). If provided an invalid axis, the function must raise an exception. Default: None. - keepdims : bool, optional - If True, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, - accordingly, the result must be compatible with the input array (see Broadcasting). Otherwise, if False, - the reduced axes (dimensions) must not be included in the result. Default: False. - - Returns - ------- - out : Array - If a logical AND reduction was performed over the entire array, the returned array must be a zero-dimensional - array containing the test result; otherwise, the returned array must be a non-zero-dimensional array - containing the test results. The returned array must have a data type of bool. - - Note - ---- - - Positive infinity, negative infinity, and NaN must evaluate to True. - - If x has a complex floating-point data type, elements having a non-zero component (real or imaginary) must - evaluate to True. - - If x is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test - result must be True. - """ return NotImplemented def any(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: - """ - Tests whether any input array element evaluates to True along a specified axis. - - Parameters - ---------- - x : Array - Input array. - axis : None | int | tuple[int, ...], optional - Axis or axes along which to perform a logical OR reduction. By default, a logical OR reduction must be - performed over the entire array. If a tuple of integers, logical OR reductions must be performed over - multiple axes. A valid axis must be an integer on the interval [-N, N), where N is the rank (number of - dimensions) of x. If an axis is specified as a negative integer, the function must determine the axis along - which to perform a reduction by counting backward from the last dimension (where -1 refers to the last - dimension). If provided an invalid axis, the function must raise an exception. Default: None. - keepdims : bool, optional - If True, the reduced axes (dimensions) must be included in the result as singleton dimensions, and, - accordingly, the result must be compatible with the input array (see Broadcasting). Otherwise, if False, - the reduced axes (dimensions) must not be included in the result. Default: False. - - Returns - ------- - out : Array - If a logical OR reduction was performed over the entire array, the returned array must be a zero-dimensional - array containing the test result; otherwise, the returned array must be a non-zero-dimensional array - containing the test results. The returned array must have a data type of bool. - - Note - ---- - - Positive infinity, negative infinity, and NaN must evaluate to True. - - If x has a complex floating-point data type, elements having a non-zero component (real or imaginary) must - evaluate to True. - - If x is an empty array or the size of the axis (dimension) along which to evaluate elements is zero, the test - result must be False. - """ return NotImplemented From 908447bea4e0c5bca88233841cf350112514112a Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 5 Feb 2023 00:22:03 +0200 Subject: [PATCH 23/29] Change tags and typings --- arrayfire/array_api/array_object.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 0df29c267..294f2e451 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -6,9 +6,10 @@ from dataclasses import dataclass from typing import Any -from arrayfire import backend, safe_call # TODO refactor -from arrayfire.algorithm import count # TODO refactor -from arrayfire.array import _get_indices, _in_display_dims_limit # TODO refactor +# TODO replace imports from original lib with refactored ones +from arrayfire import backend, safe_call +from arrayfire.algorithm import count +from arrayfire.array import _get_indices, _in_display_dims_limit from .device import PointerSource from .dtypes import CShape, Dtype @@ -630,35 +631,35 @@ def __iadd__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_add) - def __isub__(self, other: int | float | bool | complex | Array, /) -> Array: + def __isub__(self, other: int | float | Array, /) -> Array: """ Return self -= other. """ return _process_c_function(self, other, backend.get().af_sub) - def __imul__(self, other: int | float | bool | complex | Array, /) -> Array: + def __imul__(self, other: int | float | Array, /) -> Array: """ Return self *= other. """ return _process_c_function(self, other, backend.get().af_mul) - def __itruediv__(self, other: int | float | bool | complex | Array, /) -> Array: + def __itruediv__(self, other: int | float | Array, /) -> Array: """ Return self /= other. """ return _process_c_function(self, other, backend.get().af_div) - def __ifloordiv__(self, other: int | float | bool | complex | Array, /) -> Array: + def __ifloordiv__(self, other: int | float | Array, /) -> Array: # TODO return NotImplemented - def __imod__(self, other: int | float | bool | complex | Array, /) -> Array: + def __imod__(self, other: int | float | Array, /) -> Array: """ Return self %= other. """ return _process_c_function(self, other, backend.get().af_mod) - def __ipow__(self, other: int | float | bool | complex | Array, /) -> Array: + def __ipow__(self, other: int | float | Array, /) -> Array: """ Return self **= other. """ @@ -1078,10 +1079,12 @@ def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: D elif dtype == af_int64: # TODO discuss workaround for passing float to ctypes safe_call(backend.get().af_constant_long( - ctypes.pointer(out.arr), ctypes.c_longlong(value.real), 4, ctypes.pointer(shape.c_array))) + ctypes.pointer(out.arr), ctypes.c_longlong(value.real), # type: ignore[arg-type] + 4, ctypes.pointer(shape.c_array))) elif dtype == af_uint64: safe_call(backend.get().af_constant_ulong( - ctypes.pointer(out.arr), ctypes.c_ulonglong(value.real), 4, ctypes.pointer(shape.c_array))) + ctypes.pointer(out.arr), ctypes.c_ulonglong(value.real), # type: ignore[arg-type] + 4, ctypes.pointer(shape.c_array))) else: safe_call(backend.get().af_constant( ctypes.pointer(out.arr), ctypes.c_double(value), 4, ctypes.pointer(shape.c_array), dtype.c_api_value)) From fa3ad061195927eb2e562983780bbda359d8b9aa Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 5 Feb 2023 00:41:18 +0200 Subject: [PATCH 24/29] Change typings from python 3.10 to python 3.8 --- arrayfire/array_api/array_object.py | 143 ++++++++++++++-------------- arrayfire/array_api/dtypes.py | 14 --- 2 files changed, 72 insertions(+), 85 deletions(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 294f2e451..30977895f 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -4,7 +4,7 @@ import ctypes import enum from dataclasses import dataclass -from typing import Any +from typing import Any, List, Optional, Tuple, Union # TODO replace imports from original lib with refactored ones from arrayfire import backend, safe_call @@ -23,7 +23,7 @@ from .dtypes import supported_dtypes from .dtypes import uint64 as af_uint64 -ShapeType = tuple[int, ...] +ShapeType = Tuple[int, ...] # HACK, TODO replace for actual bcast_var after refactoring ~ https://github.com/arrayfire/arrayfire/pull/2871 _bcast_var = False @@ -32,16 +32,16 @@ @dataclass class _ArrayBuffer: - address: int | None = None + address: Optional[int] = None length: int = 0 class Array: def __init__( - self, x: None | Array | py_array.array | int | ctypes.c_void_p | list = None, - dtype: None | Dtype | str = None, shape: None | ShapeType = None, - pointer_source: PointerSource = PointerSource.host, offset: None | ctypes._SimpleCData[int] = None, - strides: None | ShapeType = None) -> None: + 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: Optional[ShapeType] = None, + pointer_source: PointerSource = PointerSource.host, offset: Optional[ctypes._SimpleCData[int]] = None, + strides: Optional[ShapeType] = None) -> None: _no_initial_dtype = False # HACK, FIXME # Initialise array object @@ -161,7 +161,7 @@ def __neg__(self) -> Array: """ return 0 - self # type: ignore[no-any-return, operator] # FIXME - def __add__(self, other: int | float | Array, /) -> Array: + 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. @@ -169,7 +169,7 @@ def __add__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance (augend array). Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Addend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -180,7 +180,7 @@ def __add__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_add) - def __sub__(self, other: int | float | Array, /) -> Array: + 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. @@ -191,7 +191,7 @@ def __sub__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance (minuend array). Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Subtrahend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -202,7 +202,7 @@ def __sub__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_sub) - def __mul__(self, other: int | float | Array, /) -> Array: + 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. @@ -210,7 +210,7 @@ def __mul__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -221,7 +221,7 @@ def __mul__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_mul) - def __truediv__(self, other: int | float | Array, /) -> Array: + 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. @@ -230,7 +230,7 @@ def __truediv__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -248,11 +248,11 @@ def __truediv__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_div) - def __floordiv__(self, other: int | float | Array, /) -> Array: + def __floordiv__(self, other: Union[int, float, Array], /) -> Array: # TODO return NotImplemented - def __mod__(self, other: int | float | Array, /) -> Array: + 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. @@ -261,7 +261,7 @@ def __mod__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a real-valued data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. Returns @@ -278,7 +278,7 @@ def __mod__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_mod) - def __pow__(self, other: int | float | Array, /) -> Array: + 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 @@ -288,7 +288,7 @@ def __pow__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance whose elements correspond to the exponentiation base. Should have a numeric data type. - other: int | float | Array + 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. @@ -326,7 +326,7 @@ def __invert__(self) -> Array: safe_call(backend.get().af_bitnot(ctypes.pointer(out.arr), self.arr)) return out - def __and__(self, other: int | bool | Array, /) -> Array: + 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. @@ -335,7 +335,7 @@ def __and__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | bool | Array + other: Union[int, bool, Array] Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -346,7 +346,7 @@ def __and__(self, other: int | bool | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_bitand) - def __or__(self, other: int | bool | Array, /) -> Array: + 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. @@ -355,7 +355,7 @@ def __or__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | bool | Array + other: Union[int, bool, Array] Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -366,7 +366,7 @@ def __or__(self, other: int | bool | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_bitor) - def __xor__(self, other: int | bool | Array, /) -> Array: + 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. @@ -375,7 +375,7 @@ def __xor__(self, other: int | bool | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | bool | Array + other: Union[int, bool, Array] Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. Returns @@ -386,7 +386,7 @@ def __xor__(self, other: int | bool | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_bitxor) - def __lshift__(self, other: int | Array, /) -> Array: + 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. @@ -395,7 +395,7 @@ def __lshift__(self, other: int | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | Array + 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. @@ -406,7 +406,7 @@ def __lshift__(self, other: int | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_bitshiftl) - def __rshift__(self, other: int | Array, /) -> Array: + 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. @@ -415,7 +415,7 @@ def __rshift__(self, other: int | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | Array + 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. @@ -428,7 +428,7 @@ def __rshift__(self, other: int | Array, /) -> Array: # Comparison Operators - def __lt__(self, other: int | float | Array, /) -> Array: + 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. @@ -437,7 +437,7 @@ def __lt__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. Returns @@ -447,7 +447,7 @@ def __lt__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_lt) - def __le__(self, other: int | float | Array, /) -> Array: + 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. @@ -456,7 +456,7 @@ def __le__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. Returns @@ -466,7 +466,7 @@ def __le__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_le) - def __gt__(self, other: int | float | Array, /) -> Array: + 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. @@ -475,7 +475,7 @@ def __gt__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. Returns @@ -485,7 +485,7 @@ def __gt__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_gt) - def __ge__(self, other: int | float | Array, /) -> Array: + 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. @@ -494,7 +494,7 @@ def __ge__(self, other: int | float | Array, /) -> Array: ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | Array + other: Union[int, float, Array] Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. Returns @@ -504,7 +504,7 @@ def __ge__(self, other: int | float | Array, /) -> Array: """ return _process_c_function(self, other, backend.get().af_ge) - def __eq__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME + def __eq__(self, other: Union[int, float, bool, Array], /) -> Array: # type: ignore[override] # FIXME """ Computes the truth value of self_i == other_i for each element of an array instance with the respective element of the array other. @@ -513,7 +513,7 @@ def __eq__(self, other: int | float | bool | Array, /) -> Array: # type: ignore ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | bool | Array + other: Union[int, float, bool, Array] Other array. Must be compatible with self (see Broadcasting). May have any data type. Returns @@ -523,7 +523,7 @@ def __eq__(self, other: int | float | bool | Array, /) -> Array: # type: ignore """ return _process_c_function(self, other, backend.get().af_eq) - def __ne__(self, other: int | float | bool | Array, /) -> Array: # type: ignore[override] # FIXME + def __ne__(self, other: Union[int, float, bool, Array], /) -> Array: # type: ignore[override] # FIXME """ Computes the truth value of self_i != other_i for each element of an array instance with the respective element of the array other. @@ -532,7 +532,7 @@ def __ne__(self, other: int | float | bool | Array, /) -> Array: # type: ignore ---------- self : Array Array instance. Should have a numeric data type. - other: int | float | bool | Array + other: Union[int, float, bool, Array] Other array. Must be compatible with self (see Broadcasting). May have any data type. Returns @@ -624,42 +624,42 @@ def __rrshift__(self, other: Array, /) -> Array: # In-place Arithmetic Operators - def __iadd__(self, other: int | float | Array, /) -> Array: + 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.get().af_add) - def __isub__(self, other: int | float | Array, /) -> Array: + def __isub__(self, other: Union[int, float, Array], /) -> Array: """ Return self -= other. """ return _process_c_function(self, other, backend.get().af_sub) - def __imul__(self, other: int | float | Array, /) -> Array: + def __imul__(self, other: Union[int, float, Array], /) -> Array: """ Return self *= other. """ return _process_c_function(self, other, backend.get().af_mul) - def __itruediv__(self, other: int | float | Array, /) -> Array: + def __itruediv__(self, other: Union[int, float, Array], /) -> Array: """ Return self /= other. """ return _process_c_function(self, other, backend.get().af_div) - def __ifloordiv__(self, other: int | float | Array, /) -> Array: + def __ifloordiv__(self, other: Union[int, float, Array], /) -> Array: # TODO return NotImplemented - def __imod__(self, other: int | float | Array, /) -> Array: + def __imod__(self, other: Union[int, float, Array], /) -> Array: """ Return self %= other. """ return _process_c_function(self, other, backend.get().af_mod) - def __ipow__(self, other: int | float | Array, /) -> Array: + def __ipow__(self, other: Union[int, float, Array], /) -> Array: """ Return self **= other. """ @@ -673,31 +673,31 @@ def __imatmul__(self, other: Array, /) -> Array: # In-place Bitwise Operators - def __iand__(self, other: int | bool | Array, /) -> Array: + def __iand__(self, other: Union[int, bool, Array], /) -> Array: """ Return self &= other. """ return _process_c_function(self, other, backend.get().af_bitand) - def __ior__(self, other: int | bool | Array, /) -> Array: + def __ior__(self, other: Union[int, bool, Array], /) -> Array: """ Return self |= other. """ return _process_c_function(self, other, backend.get().af_bitor) - def __ixor__(self, other: int | bool | Array, /) -> Array: + def __ixor__(self, other: Union[int, bool, Array], /) -> Array: """ Return self ^= other. """ return _process_c_function(self, other, backend.get().af_bitxor) - def __ilshift__(self, other: int | Array, /) -> Array: + def __ilshift__(self, other: Union[int, Array], /) -> Array: """ Return self <<= other. """ return _process_c_function(self, other, backend.get().af_bitshiftl) - def __irshift__(self, other: int | Array, /) -> Array: + def __irshift__(self, other: Union[int, Array], /) -> Array: """ Return self >>= other. """ @@ -709,7 +709,7 @@ def __abs__(self) -> Array: # TODO return NotImplemented - def __array_namespace__(self, *, api_version: None | str = None) -> Any: + def __array_namespace__(self, *, api_version: Optional[str] = None) -> Any: # TODO return NotImplemented @@ -721,11 +721,11 @@ def __complex__(self) -> complex: # TODO return NotImplemented - def __dlpack__(self, *, stream: None | int | Any = None): # type: ignore[no-untyped-def] + 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]: + def __dlpack_device__(self) -> Tuple[enum.Enum, int]: # TODO return NotImplemented @@ -733,7 +733,7 @@ def __float__(self) -> float: # TODO return NotImplemented - def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array: + def __getitem__(self, key: Union[int, slice, Tuple[Union[int, slice, ], ...], Array], /) -> Array: """ Returns self[key]. @@ -741,7 +741,7 @@ def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array ---------- self : Array Array instance. - key : int | slice | tuple[int | slice] | Array + key : Union[int, slice, Tuple[Union[int, slice, ], ...], Array] Index key. Returns @@ -750,7 +750,7 @@ def __getitem__(self, key: int | slice | tuple[int | slice] | Array, /) -> Array An array containing the accessed value(s). The returned array must have the same data type as self. """ # TODO - # API Specification - key: int | slice | ellipsis | tuple[int | slice] | Array. + # 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 @@ -776,7 +776,8 @@ def __len__(self) -> int: return self.shape[0] if self.shape else 0 def __setitem__( - self, key: int | slice | tuple[int | slice, ...] | Array, value: int | float | bool | Array, /) -> None: + 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 @@ -792,7 +793,7 @@ def __repr__(self) -> str: # TODO change the look of array representation. E.g., like np.array return _array_as_str(self) - def to_device(self, device: Any, /, *, stream: None | int | Any = None) -> Array: + def to_device(self, device: Any, /, *, stream: Union[int, Any] = None) -> Array: # TODO implementation and change device type from Any to Device return NotImplemented @@ -896,7 +897,7 @@ def shape(self) -> ShapeType: ctypes.pointer(d0), ctypes.pointer(d1), ctypes.pointer(d2), ctypes.pointer(d3), self.arr)) return (d0.value, d1.value, d2.value, d3.value)[:self.ndim] # Skip passing None values - def scalar(self) -> None | int | float | bool | complex: + def scalar(self) -> Union[None, int, float, bool, complex]: """ Return the first element of the array """ @@ -916,7 +917,7 @@ def is_empty(self) -> bool: safe_call(backend.get().af_is_empty(ctypes.pointer(out), self.arr)) return out.value - def to_list(self, row_major: bool = False) -> list: # FIXME return typings + def to_list(self, row_major: bool = False) -> List[Union[None, int, float, bool, complex]]: if self.is_empty(): return [] @@ -974,14 +975,14 @@ def _array_as_str(array: Array) -> str: return py_str -def _metadata_string(dtype: Dtype, dims: None | ShapeType = None) -> str: +def _metadata_string(dtype: Dtype, dims: Optional[ShapeType] = None) -> str: return ( "arrayfire.Array()\n" f"Type: {dtype.typename}\n" f"Dims: {str(dims) if dims else ''}") -def _get_cshape(shape: None | ShapeType, buffer_length: int) -> CShape: +def _get_cshape(shape: Optional[ShapeType], buffer_length: int) -> CShape: if shape: return CShape(*shape) @@ -1012,7 +1013,7 @@ def _str_to_dtype(value: int) -> Dtype: def _process_c_function( - lhs: int | float | bool | complex | Array, rhs: int | float | bool | complex | Array, + lhs: Union[int, float, Array], rhs: Union[int, float, Array], c_function: Any) -> Array: out = Array() @@ -1020,14 +1021,14 @@ def _process_c_function( lhs_array = lhs.arr rhs_array = rhs.arr - elif isinstance(lhs, Array) and isinstance(rhs, int | float | bool | complex): + elif isinstance(lhs, Array) and isinstance(rhs, (int, float)): rhs_dtype = _implicit_dtype(rhs, lhs.dtype) rhs_constant_array = _constant_array(rhs, CShape(*lhs.shape), rhs_dtype) lhs_array = lhs.arr rhs_array = rhs_constant_array.arr - elif isinstance(lhs, int | float | bool | complex) and isinstance(rhs, Array): + elif isinstance(lhs, (int, float)) and isinstance(rhs, Array): lhs_dtype = _implicit_dtype(lhs, rhs.dtype) lhs_constant_array = _constant_array(lhs, CShape(*rhs.shape), lhs_dtype) @@ -1042,7 +1043,7 @@ def _process_c_function( return out -def _implicit_dtype(value: int | float | bool | complex, array_dtype: Dtype) -> Dtype: +def _implicit_dtype(value: Union[int, float], array_dtype: Dtype) -> Dtype: if isinstance(value, bool): value_dtype = af_bool if isinstance(value, int): @@ -1066,7 +1067,7 @@ def _implicit_dtype(value: int | float | bool | complex, array_dtype: Dtype) -> return value_dtype -def _constant_array(value: int | float | bool | complex, shape: CShape, dtype: Dtype) -> Array: +def _constant_array(value: Union[int, float], shape: CShape, dtype: Dtype) -> Array: out = Array() if isinstance(value, complex): diff --git a/arrayfire/array_api/dtypes.py b/arrayfire/array_api/dtypes.py index 7f099730f..0059bf9cf 100644 --- a/arrayfire/array_api/dtypes.py +++ b/arrayfire/array_api/dtypes.py @@ -55,17 +55,3 @@ def __repr__(self) -> str: 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)) - - -# @safe_call -# def backend() -# ... - -# @backend(safe=True) -# def af_get_type(arr) -> ...: -# dty = ctypes.c_int() -# safe_call(backend.get().af_get_type(ctypes.pointer(dty), self.arr)) # -> new dty -# return dty - -# def new_dtype(): -# return af_get_type(self.arr) From 0de99554acd96ecf685c09c462d35a4d37a3e596 Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 5 Feb 2023 01:18:05 +0200 Subject: [PATCH 25/29] Add readme with reference to run tests --- arrayfire/array_api/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 arrayfire/array_api/README.md diff --git a/arrayfire/array_api/README.md b/arrayfire/array_api/README.md new file mode 100644 index 000000000..df444ed68 --- /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 +``` From ae6be056ee6cb6e13f41697c076bae6a343ed4ba Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 5 Feb 2023 05:01:09 +0200 Subject: [PATCH 26/29] Revert changes accidentally made in original array --- arrayfire/array.py | 67 +++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/arrayfire/array.py b/arrayfire/array.py index bf71ac227..1b71db2c7 100644 --- a/arrayfire/array.py +++ b/arrayfire/array.py @@ -11,8 +11,6 @@ Array class and helper functions. """ -from .algorithm import sum, count -from .arith import cast import inspect import os from .library import * @@ -27,7 +25,6 @@ _display_dims_limit = None - def set_display_dims_limit(*dims): """ Sets the dimension limit after which array's data won't get @@ -47,7 +44,6 @@ def set_display_dims_limit(*dims): global _display_dims_limit _display_dims_limit = dims - def get_display_dims_limit(): """ Gets the dimension limit after which array's data won't get @@ -71,7 +67,6 @@ def get_display_dims_limit(): """ return _display_dims_limit - def _in_display_dims_limit(dims): if _is_running_in_py_charm: return False @@ -85,7 +80,6 @@ def _in_display_dims_limit(dims): return False return True - def _create_array(buf, numdims, idims, dtype, is_device): out_arr = c_void_ptr_t(0) c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) @@ -97,7 +91,6 @@ def _create_array(buf, numdims, idims, dtype, is_device): numdims, c_pointer(c_dims), dtype.value)) return out_arr - def _create_strided_array(buf, numdims, idims, dtype, is_device, offset, strides): out_arr = c_void_ptr_t(0) c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) @@ -119,15 +112,16 @@ def _create_strided_array(buf, numdims, idims, dtype, is_device, offset, strides location.value)) return out_arr - def _create_empty_array(numdims, idims, dtype): out_arr = c_void_ptr_t(0) + + if numdims == 0: return out_arr + c_dims = dim4(idims[0], idims[1], idims[2], idims[3]) safe_call(backend.get().af_create_handle(c_pointer(out_arr), numdims, c_pointer(c_dims), dtype.value)) return out_arr - def constant_array(val, d0, d1=None, d2=None, d3=None, dtype=Dtype.f32): """ Internal function to create a C array. Should not be used externall. @@ -182,7 +176,6 @@ def _binary_func(lhs, rhs, c_func): return out - def _binary_funcr(lhs, rhs, c_func): out = Array() other = lhs @@ -199,10 +192,9 @@ def _binary_funcr(lhs, rhs, c_func): return out - def _ctype_to_lists(ctype_arr, dim, shape, offset=0): if (dim == 0): - return list(ctype_arr[offset: offset + shape[0]]) + return list(ctype_arr[offset : offset + shape[0]]) else: dim_len = shape[dim] res = [[]] * dim_len @@ -211,7 +203,6 @@ def _ctype_to_lists(ctype_arr, dim, shape, offset=0): offset += shape[0] return res - def _slice_to_length(key, dim): tkey = [key.start, key.stop, key.step] @@ -230,7 +221,6 @@ def _slice_to_length(key, dim): return int(((tkey[1] - tkey[0] - 1) / tkey[2]) + 1) - def _get_info(dims, buf_len): elements = 1 numdims = 0 @@ -260,7 +250,6 @@ def _get_indices(key): return inds - def _get_assign_dims(key, idims): dims = [1]*4 @@ -307,7 +296,6 @@ def _get_assign_dims(key, idims): else: raise IndexError("Invalid type while assigning to arrayfire.array") - def transpose(a, conj=False): """ Perform the transpose on an input. @@ -330,7 +318,6 @@ def transpose(a, conj=False): safe_call(backend.get().af_transpose(c_pointer(out.arr), a.arr, conj)) return out - def transpose_inplace(a, conj=False): """ Perform inplace transpose on an input. @@ -351,7 +338,6 @@ def transpose_inplace(a, conj=False): """ safe_call(backend.get().af_transpose_inplace(a.arr, conj)) - class Array(BaseArray): """ @@ -461,8 +447,8 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None super(Array, self).__init__() - buf = None - buf_len = 0 + buf=None + buf_len=0 if dtype is not None: if isinstance(dtype, str): @@ -472,7 +458,7 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None else: type_char = None - _type_char = 'f' + _type_char='f' if src is not None: @@ -483,12 +469,12 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None host = __import__("array") if isinstance(src, host.array): - buf, buf_len = src.buffer_info() + buf,buf_len = src.buffer_info() _type_char = src.typecode numdims, idims = _get_info(dims, buf_len) elif isinstance(src, list): tmp = host.array('f', src) - buf, buf_len = tmp.buffer_info() + buf,buf_len = tmp.buffer_info() _type_char = tmp.typecode numdims, idims = _get_info(dims, buf_len) elif isinstance(src, int) or isinstance(src, c_void_ptr_t): @@ -512,7 +498,7 @@ def __init__(self, src=None, dims=None, dtype=None, is_device=False, offset=None raise TypeError("src is an object of unsupported class") if (type_char is not None and - type_char != _type_char): + type_char != _type_char): raise TypeError("Can not create array of requested type from input data type") if(offset is None and strides is None): self.arr = _create_array(buf, numdims, idims, to_dtype[_type_char], is_device) @@ -634,8 +620,8 @@ def strides(self): s2 = c_dim_t(0) s3 = c_dim_t(0) safe_call(backend.get().af_get_strides(c_pointer(s0), c_pointer(s1), - c_pointer(s2), c_pointer(s3), self.arr)) - strides = (s0.value, s1.value, s2.value, s3.value) + c_pointer(s2), c_pointer(s3), self.arr)) + strides = (s0.value,s1.value,s2.value,s3.value) return strides[:self.numdims()] def elements(self): @@ -694,8 +680,8 @@ def dims(self): d2 = c_dim_t(0) d3 = c_dim_t(0) safe_call(backend.get().af_get_dims(c_pointer(d0), c_pointer(d1), - c_pointer(d2), c_pointer(d3), self.arr)) - dims = (d0.value, d1.value, d2.value, d3.value) + c_pointer(d2), c_pointer(d3), self.arr)) + dims = (d0.value,d1.value,d2.value,d3.value) return dims[:self.numdims()] @property @@ -920,7 +906,7 @@ def __itruediv__(self, other): """ Perform self /= other. """ - self = _binary_func(self, other, backend.get().af_div) + self = _binary_func(self, other, backend.get().af_div) return self def __rtruediv__(self, other): @@ -939,7 +925,7 @@ def __idiv__(self, other): """ Perform other / self. """ - self = _binary_func(self, other, backend.get().af_div) + self = _binary_func(self, other, backend.get().af_div) return self def __rdiv__(self, other): @@ -958,7 +944,7 @@ def __imod__(self, other): """ Perform self %= other. """ - self = _binary_func(self, other, backend.get().af_mod) + self = _binary_func(self, other, backend.get().af_mod) return self def __rmod__(self, other): @@ -977,7 +963,7 @@ def __ipow__(self, other): """ Perform self **= other. """ - self = _binary_func(self, other, backend.get().af_pow) + self = _binary_func(self, other, backend.get().af_pow) return self def __rpow__(self, other): @@ -1120,7 +1106,7 @@ def logical_and(self, other): Return self && other. """ out = Array() - safe_call(backend.get().af_and(c_pointer(out.arr), self.arr, other.arr)) # TODO: bcast var? + safe_call(backend.get().af_and(c_pointer(out.arr), self.arr, other.arr)) #TODO: bcast var? return out def logical_or(self, other): @@ -1128,7 +1114,7 @@ def logical_or(self, other): Return self || other. """ out = Array() - safe_call(backend.get().af_or(c_pointer(out.arr), self.arr, other.arr)) # TODO: bcast var? + safe_call(backend.get().af_or(c_pointer(out.arr), self.arr, other.arr)) #TODO: bcast var? return out def __nonzero__(self): @@ -1158,11 +1144,12 @@ def __getitem__(self, key): inds = _get_indices(key) safe_call(backend.get().af_index_gen(c_pointer(out.arr), - self.arr, c_dim_t(n_dims), inds.pointer)) + self.arr, c_dim_t(n_dims), inds.pointer)) return out except RuntimeError as e: raise IndexError(str(e)) + def __setitem__(self, key, val): """ Perform self[key] = val @@ -1188,14 +1175,14 @@ def __setitem__(self, key, val): n_dims = 1 other_arr = constant_array(val, int(num), dtype=self.type()) else: - other_arr = constant_array(val, tdims[0], tdims[1], tdims[2], tdims[3], self.type()) + other_arr = constant_array(val, tdims[0] , tdims[1], tdims[2], tdims[3], self.type()) del_other = True else: other_arr = val.arr del_other = False out_arr = c_void_ptr_t(0) - inds = _get_indices(key) + inds = _get_indices(key) safe_call(backend.get().af_assign_gen(c_pointer(out_arr), self.arr, c_dim_t(n_dims), inds.pointer, @@ -1414,7 +1401,6 @@ def to_ndarray(self, output=None): safe_call(backend.get().af_get_data_ptr(c_void_ptr_t(output.ctypes.data), tmp.arr)) return output - def display(a, precision=4): """ Displays the contents of an array. @@ -1440,7 +1426,6 @@ def display(a, precision=4): safe_call(backend.get().af_print_array_gen(name.encode('utf-8'), a.arr, c_int_t(precision))) - def save_array(key, a, filename, append=False): """ Save an array to disk. @@ -1472,7 +1457,6 @@ def save_array(key, a, filename, append=False): append)) return index.value - def read_array(filename, index=None, key=None): """ Read an array from disk. @@ -1506,3 +1490,6 @@ def read_array(filename, index=None, key=None): key.encode('utf-8'))) return out + +from .algorithm import (sum, count) +from .arith import cast From 15bc8cbec21decae69315be1f8b2015caa13fafd Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 25 Apr 2023 20:05:51 +0300 Subject: [PATCH 27/29] Add constructor initialisation warning. Add Note on deviation from spec. Dump minimal numpy version required. --- arrayfire/array_api/array_object.py | 7 ++++++- setup.cfg | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index 30977895f..fd9b73f83 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -3,6 +3,7 @@ import array as py_array import ctypes import enum +import warnings from dataclasses import dataclass from typing import Any, List, Optional, Tuple, Union @@ -43,6 +44,9 @@ def __init__( pointer_source: PointerSource = PointerSource.host, offset: Optional[ctypes._SimpleCData[int]] = None, strides: Optional[ShapeType] = None) -> None: _no_initial_dtype = False # HACK, FIXME + warnings.warn( + "Initialisation with __init__ constructor is about to be deprecated and replaced with asarray() method.", + DeprecationWarning, stacklevel=2) # Initialise array object self.arr = ctypes.c_void_p(0) @@ -773,6 +777,7 @@ def __int__(self) -> int: return NotImplemented def __len__(self) -> int: + # NOTE a part of the array-api spec return self.shape[0] if self.shape else 0 def __setitem__( @@ -989,7 +994,7 @@ def _get_cshape(shape: Optional[ShapeType], buffer_length: int) -> CShape: if buffer_length != 0: return CShape(buffer_length) - raise RuntimeError("Shape and buffer length are size invalid.") + raise RuntimeError("Shape and buffer length have invalid size to process them into C shape.") def _c_api_value_to_dtype(value: int) -> Dtype: diff --git a/setup.cfg b/setup.cfg index 84f512c2e..b40ac55e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ exclude = examples tests install_requires = - numpy~=1.23.4 + numpy~=1.22.0 [options.extras_require] dev = From e24e4780654bd2cbee3f580ef01c93f9207d4d41 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 25 Apr 2023 20:13:31 +0300 Subject: [PATCH 28/29] Fix warning message --- arrayfire/array_api/array_object.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index fd9b73f83..be99f465d 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -45,7 +45,8 @@ def __init__( strides: Optional[ShapeType] = None) -> None: _no_initial_dtype = False # HACK, FIXME warnings.warn( - "Initialisation with __init__ constructor is about to be deprecated and replaced with asarray() method.", + "Initialisation with __init__ constructor is not a part of array-api specification" + " and about to be replaced with asarray() method.", DeprecationWarning, stacklevel=2) # Initialise array object From f3a80c02143db5da075f74395cd91b22eebd9431 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 25 Apr 2023 20:19:08 +0300 Subject: [PATCH 29/29] Add NOTE tag to functions that are not a part of spec but custom solutions --- arrayfire/array_api/array_object.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py index be99f465d..616dca797 100644 --- a/arrayfire/array_api/array_object.py +++ b/arrayfire/array_api/array_object.py @@ -778,7 +778,7 @@ def __int__(self) -> int: return NotImplemented def __len__(self) -> int: - # NOTE a part of the array-api spec + # NOTE not a part of the array-api spec return self.shape[0] if self.shape else 0 def __setitem__( @@ -788,6 +788,7 @@ def __setitem__( return NotImplemented # type: ignore[return-value] # FIXME def __str__(self) -> str: + # NOTE not a part of the array-api spec # 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) @@ -795,6 +796,7 @@ def __str__(self) -> str: return _metadata_string(self.dtype) + _array_as_str(self) def __repr__(self) -> str: + # NOTE not a part of the array-api spec # return _metadata_string(self.dtype, self.shape) # TODO change the look of array representation. E.g., like np.array return _array_as_str(self) @@ -907,6 +909,7 @@ def scalar(self) -> Union[None, int, float, bool, complex]: """ Return the first element of the array """ + # NOTE not a part of the array-api spec # TODO change the logic of this method if self.is_empty(): return None @@ -919,11 +922,13 @@ def is_empty(self) -> bool: """ Check if the array is empty i.e. it has no elements. """ + # NOTE not a part of the array-api spec out = ctypes.c_bool() safe_call(backend.get().af_is_empty(ctypes.pointer(out), self.arr)) return out.value def to_list(self, row_major: bool = False) -> List[Union[None, int, float, bool, complex]]: + # NOTE not a part of the array-api spec if self.is_empty(): return [] @@ -945,6 +950,7 @@ def to_list(self, row_major: bool = False) -> List[Union[None, int, float, bool, return out def to_ctype_array(self, row_major: bool = False) -> ctypes.Array: + # NOTE not a part of the array-api spec if self.is_empty(): raise RuntimeError("Can not convert an empty array to ctype.")