diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..c3ca58a39 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,29 @@ +name: Run Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python 3.10 + uses: actions/setup-python@v1 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Test with pytest + run: pytest + + - name: Install AF + run: apt install arrayfire + + - name: Test array_api + run: python -m pytest arrayfire/array_api diff --git a/.gitignore b/.gitignore index 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/.gitmodules b/.gitmodules new file mode 100644 index 000000000..154589c05 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "assets"] + path = assets + url = https://github.com/arrayfire/assets.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..4c1808108 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.11.0) +project(arrayfire-python) +find_package(PythonExtensions REQUIRED) +include(FetchContent) + +set(CMAKE_MODULE_PATH_OLD ${CMAKE_MODULE_PATH}) +set(CMAKE_MODULE_PATH "") +set(NO_SONAME) + +FetchContent_Declare( + arrayfire + GIT_REPOSITORY https://github.com/arrayfire/arrayfire.git + GIT_TAG v3.8 +) +FetchContent_MakeAvailable(arrayfire) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH_OLD}) + +set(ignoreWarning "${SKBUILD}") diff --git a/README.md b/README.md index 2d3f8c05a..737399adb 100644 --- a/README.md +++ b/README.md @@ -25,42 +25,42 @@ def calc_pi_device(samples): Choosing a particular backend can be done using `af.set_backend(name)` where name is either "_cuda_", "_opencl_", or "_cpu_". The default device is chosen in the same order of preference. -## Requirements - -Currently, this project is tested only on Linux and OSX. You also need to have the ArrayFire C/C++ library installed on your machine. You can get it from the following sources. +## Getting started +ArrayFire can be installed from a variety of sources. [Pre-built wheels](https://repo.arrayfire.com/python/wheels/3.8.0/) are available for a number of systems and toolkits. These will include a distribution of the ArrayFire libraries. Currently, only the python wrapper is available on PyPI. Wrapper-only installations will require a separate installation of the ArrayFire C/C++ libraries. +You can get the ArrayFire C/C++ library from the following sources: - [Download and install binaries](https://arrayfire.com/download) - [Build and install from source](https://github.com/arrayfire/arrayfire) -Please check the following links for dependencies. - -- [Linux dependencies](http://www.arrayfire.com/docs/using_on_linux.htm) -- [OSX dependencies](http://www.arrayfire.com/docs/using_on_osx.htm) - -## Getting started - -**Install the last stable version:** +**Install the last stable version of python wrapper:** ``` pip install arrayfire ``` -**Install the development version:** +**Install a pre-built wheel for a specific CUDA toolkit version:** +``` +pip install arrayfire==3.8.0+cu112 -f https://repo.arrayfire.com/python/wheels/3.8.0/ +# Replace the +cu112 local version with the desired toolkit +``` + +**Install the development source distribution:** ``` -pip install git+git://github.com/arrayfire/arrayfire-python.git@devel +pip install git+git://github.com/arrayfire/arrayfire-python.git@master ``` -**Installing offline** +**Installing offline:** ``` cd path/to/arrayfire-python python setup.py install ``` +Rather than installing and building ArrayFire elsewhere in the system, you can also build directly through python by first setting the `AF_BUILD_LOCAL_LIBS=1` environment variable. Additional setup will be required to build ArrayFire, including satisfying dependencies and further CMake configuration. Details on how to pass additional arguments to the build systems can be found in the [scikit-build documentation.](https://scikit-build.readthedocs.io/en/latest/) -**Post Installation** +**Post Installation:** -Please follow [these instructions](https://github.com/arrayfire/arrayfire-python/wiki) to ensure the arrayfire-python can find the arrayfire libraries. +If you are not using one of the pre-built wheels, you may need to ensure arrayfire-python can find the installed arrayfire libraries. Please follow [these instructions](https://github.com/arrayfire/arrayfire-python/wiki) to ensure that arrayfire-python can find the arrayfire libraries. To run arrayfire tests, you can run the following command from command line. diff --git a/__af_version__.py b/__af_version__.py index 9b8a5f40c..16f06c5ae 100644 --- a/__af_version__.py +++ b/__af_version__.py @@ -9,6 +9,6 @@ # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## -version = "3.5" -release = "20170718" +version = "3.8" +release = "20210303" full_version = version + "." + release diff --git a/arrayfire/__init__.py b/arrayfire/__init__.py index 538085bb3..5cb009951 100644 --- a/arrayfire/__init__.py +++ b/arrayfire/__init__.py @@ -74,6 +74,7 @@ from .timer import * from .random import * from .sparse import * +from .ml import * # do not export default modules as part of arrayfire del ct diff --git a/arrayfire/algorithm.py b/arrayfire/algorithm.py index 36b5d79b8..6f06cee14 100644 --- a/arrayfire/algorithm.py +++ b/arrayfire/algorithm.py @@ -44,6 +44,31 @@ def _nan_reduce_all(a, c_func, nan_val): imag = imag.value return real if imag == 0 else real + imag * 1j +def _FNSD(dim, dims): + if dim >= 0: + return int(dim) + + fnsd = 0 + for i, d in enumerate(dims): + if d > 1: + fnsd = i + break + return int(fnsd) + +def _rbk_dim(keys, vals, dim, c_func): + keys_out = Array() + vals_out = Array() + rdim = _FNSD(dim, vals.dims()) + safe_call(c_func(c_pointer(keys_out.arr), c_pointer(vals_out.arr), keys.arr, vals.arr, c_int_t(rdim))) + return keys_out, vals_out + +def _nan_rbk_dim(a, dim, c_func, nan_val): + keys_out = Array() + vals_out = Array() + rdim = _FNSD(dim, vals.dims()) + safe_call(c_func(c_pointer(keys_out.arr), c_pointer(vals_out.arr), keys.arr, vals.arr, c_int_t(rdim), c_double_t(nan_val))) + return keys_out, vals_out + def sum(a, dim=None, nan_val=None): """ Calculate the sum of all the elements along a specified dimension. @@ -74,6 +99,34 @@ def sum(a, dim=None, nan_val=None): else: return _reduce_all(a, backend.get().af_sum_all) + +def sumByKey(keys, vals, dim=-1, nan_val=None): + """ + Calculate the sum of elements along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the sum will occur. + nan_val: optional: scalar. default: None + The value that replaces NaN in the array + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of all elements in `vals` along dimension `dim`. + values: af.Array or scalar number + The sum of all elements in `vals` along dimension `dim` according to keys + """ + if (nan_val is not None): + return _nan_rbk_dim(keys, vals, dim, backend.get().af_sum_by_key_nan, nan_val) + else: + return _rbk_dim(keys, vals, dim, backend.get().af_sum_by_key) + def product(a, dim=None, nan_val=None): """ Calculate the product of all the elements along a specified dimension. @@ -104,6 +157,33 @@ def product(a, dim=None, nan_val=None): else: return _reduce_all(a, backend.get().af_product_all) +def productByKey(keys, vals, dim=-1, nan_val=None): + """ + Calculate the product of elements along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the product will occur. + nan_val: optional: scalar. default: None + The value that replaces NaN in the array + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of all elements in `vals` along dimension `dim`. + values: af.Array or scalar number + The product of all elements in `vals` along dimension `dim` according to keys + """ + if (nan_val is not None): + return _nan_rbk_dim(keys, vals, dim, backend.get().af_product_by_key_nan, nan_val) + else: + return _rbk_dim(keys, vals, dim, backend.get().af_product_by_key) + def min(a, dim=None): """ Find the minimum value of all the elements along a specified dimension. @@ -126,6 +206,28 @@ def min(a, dim=None): else: return _reduce_all(a, backend.get().af_min_all) +def minByKey(keys, vals, dim=-1): + """ + Calculate the min of elements along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the min will occur. + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of all elements in `vals` along dimension `dim`. + values: af.Array or scalar number + The min of all elements in `vals` along dimension `dim` according to keys + """ + return _rbk_dim(keys, vals, dim, backend.get().af_min_by_key) + def max(a, dim=None): """ Find the maximum value of all the elements along a specified dimension. @@ -148,6 +250,56 @@ def max(a, dim=None): else: return _reduce_all(a, backend.get().af_max_all) +def maxRagged(vals, lens, dim): + """ + Find the maximum value of a subset of elements along a specified dimension. + + The size of the subset of elements along the given dimension are decided based on the lengths + provided in the `lens` array. + + Parameters + ---------- + vals : af.Array + Multi dimensional arrayfire array. + lens : af.Array + Multi dimensional arrayfire array containing number of elements to reduce along given `dim` + dim: optional: int. default: None + Dimension along which the maximum value is required. + + Returns + ------- + (values, indices): A tuple of af.Array(s) + `values` af.Array will have the maximum values along given dimension for + subsets determined by lengths provided in `lens` + `idx` contains the locations of the maximum values as per the lengths provided in `lens` + """ + out_vals = Array() + out_idx = Array() + safe_call(backend().get().af_max_ragged(c_pointer(out_vals.arr), c_pointer(out_idx.arr), c_pointer(vals.arr), c_pointer(lens.arr), c_int_t(dim))) + return out_vals, out_idx + +def maxByKey(keys, vals, dim=-1): + """ + Calculate the max of elements along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the max will occur. + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of all elements in `vals` along dimension `dim`. + values: af.Array or scalar number + The max of all elements in `vals` along dimension `dim` according to keys. + """ + return _rbk_dim(keys, vals, dim, backend.get().af_max_by_key) + def all_true(a, dim=None): """ Check if all the elements along a specified dimension are true. @@ -170,6 +322,28 @@ def all_true(a, dim=None): else: return _reduce_all(a, backend.get().af_all_true_all) +def allTrueByKey(keys, vals, dim=-1): + """ + Calculate if all elements are true along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the all true check will occur. + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of all true check in `vals` along dimension `dim`. + values: af.Array or scalar number + Booleans denoting if all elements are true in `vals` along dimension `dim` according to keys + """ + return _rbk_dim(keys, vals, dim, backend.get().af_all_true_by_key) + def any_true(a, dim=None): """ Check if any the elements along a specified dimension are true. @@ -192,6 +366,28 @@ def any_true(a, dim=None): else: return _reduce_all(a, backend.get().af_any_true_all) +def anyTrueByKey(keys, vals, dim=-1): + """ + Calculate if any elements are true along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which the any true check will occur. + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of any true check in `vals` along dimension `dim`. + values: af.Array or scalar number + Booleans denoting if any elements are true in `vals` along dimension `dim` according to keys. + """ + return _rbk_dim(keys, vals, dim, backend.get().af_any_true_by_key) + def count(a, dim=None): """ Count the number of non zero elements in an array along a specified dimension. @@ -214,6 +410,28 @@ def count(a, dim=None): else: return _reduce_all(a, backend.get().af_count_all) +def countByKey(keys, vals, dim=-1): + """ + Counts non-zero elements along a specified dimension according to a key. + + Parameters + ---------- + keys : af.Array + One dimensional arrayfire array with reduction keys. + vals : af.Array + Multi dimensional arrayfire array that will be reduced. + dim: optional: int. default: -1 + Dimension along which to count elements. + + Returns + ------- + keys: af.Array or scalar number + The reduced keys of count in `vals` along dimension `dim`. + values: af.Array or scalar number + Count of non-zero elements in `vals` along dimension `dim` according to keys. + """ + return _rbk_dim(keys, vals, dim, backend.get().af_count_by_key) + def imin(a, dim=None): """ Find the value and location of the minimum value along a specified dimension diff --git a/arrayfire/arith.py b/arrayfire/arith.py index b0d945f05..e4dc2fdfd 100644 --- a/arrayfire/arith.py +++ b/arrayfire/arith.py @@ -958,6 +958,26 @@ def sqrt(a): """ return _arith_unary_func(a, backend.get().af_sqrt) +def rsqrt(a): + """ + Reciprocal or inverse square root of each element in the array. + + Parameters + ---------- + a : af.Array + Multi dimensional arrayfire array. + + Returns + -------- + out : af.Array + array containing the inverse square root of each value from `a`. + + Note + ------- + `a` must not be complex. + """ + return _arith_unary_func(a, backend.get().af_rsqrt) + def cbrt(a): """ Cube root of each element in the array. diff --git a/arrayfire/array.py b/arrayfire/array.py index 76afece01..1b71db2c7 100644 --- a/arrayfire/array.py +++ b/arrayfire/array.py @@ -563,6 +563,7 @@ def device_ptr(self): Note ---- - This can be used to integrate with custom C code and / or PyCUDA or PyOpenCL. + - Implies `af.device.lock_array()`. The device pointer of `a` is not freed by memory manager until `unlock_device_ptr()` is called. - No other arrays will share the same device pointer. - A copy of the memory is done if multiple arrays share the same memory or the array is not the owner of the memory. - In case of a copy the return value points to the newly allocated memory which is now exclusively owned by the array. @@ -782,6 +783,14 @@ def is_single(self): safe_call(backend.get().af_is_single(c_pointer(res), self.arr)) return res.value + def is_half(self): + """ + Check if the array is of half floating point type (fp16). + """ + res = c_bool_t(False) + safe_call(backend.get().af_is_half(c_pointer(res), self.arr)) + return res.value + def is_real_floating(self): """ Check if the array is real and of floating point type. @@ -1077,13 +1086,36 @@ def __pos__(self): return self def __invert__(self): + """ + Return ~self + """ + out = Array() + safe_call(backend.get().af_bitnot(c_pointer(out.arr), self.arr)) + return out + + def logical_not(self): """ Return ~self """ out = Array() safe_call(backend.get().af_not(c_pointer(out.arr), self.arr)) - self = out - return self + return out + + 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? + return out + + 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? + return out def __nonzero__(self): return self != 0 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 +``` diff --git a/arrayfire/array_api/__init__.py b/arrayfire/array_api/__init__.py new file mode 100644 index 000000000..675b27ade --- /dev/null +++ b/arrayfire/array_api/__init__.py @@ -0,0 +1,9 @@ +__all__ = [ + # array objects + "Array", + # dtypes + "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", + "complex64", "complex128", "bool"] + +from .array_object import Array +from .dtypes import bool, complex64, complex128, float32, float64, int16, int32, int64, uint8, uint16, uint32, uint64 diff --git a/arrayfire/array_api/array_object.py b/arrayfire/array_api/array_object.py new file mode 100644 index 000000000..616dca797 --- /dev/null +++ b/arrayfire/array_api/array_object.py @@ -0,0 +1,1105 @@ +from __future__ import annotations + +import array as py_array +import ctypes +import enum +import warnings +from dataclasses import dataclass +from typing import Any, List, Optional, Tuple, Union + +# 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 +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 + +ShapeType = Tuple[int, ...] +# 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 +class _ArrayBuffer: + address: Optional[int] = None + length: int = 0 + + +class Array: + def __init__( + self, x: Union[None, Array, py_array.array, int, ctypes.c_void_p, List[Union[int, float]]] = None, + dtype: Union[None, Dtype, str] = None, shape: 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 + warnings.warn( + "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 + self.arr = ctypes.c_void_p(0) + + if isinstance(dtype, str): + dtype = _str_to_dtype(dtype) # type: ignore[arg-type] + + if dtype is None: + _no_initial_dtype = True + dtype = af_float32 + + if x is None: + if not shape: # shape is None or empty tuple + safe_call(backend.get().af_create_handle( + 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(CShape(*shape).c_array), 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()) + + elif isinstance(x, list): + _array = py_array.array("f", x) # BUG [True, False] -> dtype: f32 # TODO add int and float + _type_char = _array.typecode + _array_buffer = _ArrayBuffer(*_array.buffer_info()) + + elif isinstance(x, int) or isinstance(x, ctypes.c_void_p): # TODO + _array_buffer = _ArrayBuffer(x if not isinstance(x, ctypes.c_void_p) else x.value) + + if not shape: + raise TypeError("Expected to receive the initial shape due to the x being a data pointer.") + + if _no_initial_dtype: + raise TypeError("Expected to receive the initial dtype due to the x being a data pointer.") + + _type_char = dtype.typecode # type: ignore[assignment] # FIXME + + 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), _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), _cshape.original_shape, + ctypes.pointer(_cshape.c_array), dtype.c_api_value)) + return + + if offset is None: + offset = c_dim_t(0) + + 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_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, _cshape.original_shape, + ctypes.pointer(_cshape.c_array), ctypes.pointer(strides_cshape), dtype.c_api_value, + pointer_source.value)) + + # Arithmetic Operators + + def __pos__(self) -> Array: + """ + Evaluates +self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the evaluated result for each element. The returned array must have the same data type + as self. + """ + return self + + def __neg__(self) -> Array: + """ + Evaluates +self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the evaluated result for each element in self. The returned array must have a data type + determined by Type Promotion Rules. + + """ + return 0 - self # type: ignore[no-any-return, operator] # FIXME + + def __add__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the sum for each element of an array instance with the respective element of the array other. + + Parameters + ---------- + self : Array + Array instance (augend array). Should have a numeric data type. + other: Union[int, float, Array] + Addend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise sums. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_add) + + def __sub__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the difference for each element of an array instance with the respective element of the array other. + + The result of self_i - other_i must be the same as self_i + (-other_i) and must be governed by the same + floating-point rules as addition (see array.__add__()). + + Parameters + ---------- + self : Array + Array instance (minuend array). Should have a numeric data type. + other: Union[int, float, Array] + Subtrahend array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise differences. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_sub) + + def __mul__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates the product for each element of an array instance with the respective element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise products. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_mul) + + def __truediv__(self, other: Union[int, float, Array], /) -> Array: + """ + Evaluates self_i / other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array should have a floating-point data type + determined by Type Promotion Rules. + + Note + ---- + - If one or both of self and other have integer data types, the result is implementation-dependent, as type + promotion between data type “kinds” (e.g., integer versus floating-point) is unspecified. + Specification-compliant libraries may choose to raise an error or return an array containing the element-wise + results. If an array is returned, the array must have a real-valued floating-point data type. + """ + return _process_c_function(self, other, backend.get().af_div) + + def __floordiv__(self, other: Union[int, float, Array], /) -> Array: + # TODO + return NotImplemented + + def __mod__(self, other: Union[int, float, Array], /) -> Array: + """ + Evaluates self_i % other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a real-valued data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. Each element-wise result must have the same sign as the + respective element other_i. The returned array must have a real-valued floating-point data type determined + by Type Promotion Rules. + + Note + ---- + - For input arrays which promote to an integer data type, the result of division by zero is unspecified and + thus implementation-defined. + """ + return _process_c_function(self, other, backend.get().af_mod) + + def __pow__(self, other: Union[int, float, Array], /) -> Array: + """ + Calculates an implementation-dependent approximation of exponentiation by raising each element (the base) of + an array instance to the power of other_i (the exponent), where other_i is the corresponding element of the + array other. + + Parameters + ---------- + self : Array + Array instance whose elements correspond to the exponentiation base. Should have a numeric data type. + other: Union[int, float, Array] + Other array whose elements correspond to the exponentiation exponent. Must be compatible with self + (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_pow) + + # Array Operators + + def __matmul__(self, other: Array, /) -> Array: + # TODO get from blas - make vanilla version and not copy af.matmul as is + return NotImplemented + + # Bitwise Operators + + def __invert__(self) -> Array: + """ + Evaluates ~self_i for each element of an array instance. + + Parameters + ---------- + self : Array + Array instance. Should have an integer or boolean data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + out = Array() + safe_call(backend.get().af_bitnot(ctypes.pointer(out.arr), self.arr)) + return out + + def __and__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i & other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_bitand) + + def __or__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i | other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_bitor) + + def __xor__(self, other: Union[int, bool, Array], /) -> Array: + """ + Evaluates self_i ^ other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, bool, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type determined + by Type Promotion Rules. + """ + return _process_c_function(self, other, backend.get().af_bitxor) + + def __lshift__(self, other: Union[int, Array], /) -> Array: + """ + Evaluates self_i << other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + Each element must be greater than or equal to 0. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + return _process_c_function(self, other, backend.get().af_bitshiftl) + + def __rshift__(self, other: Union[int, Array], /) -> Array: + """ + Evaluates self_i >> other_i for each element of an array instance with the respective element of the + array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a numeric data type. + Each element must be greater than or equal to 0. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have the same data type as self. + """ + return _process_c_function(self, other, backend.get().af_bitshiftr) + + # Comparison Operators + + def __lt__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i < other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_lt) + + def __le__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i <= other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_le) + + def __gt__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i > other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_gt) + + def __ge__(self, other: Union[int, float, Array], /) -> Array: + """ + Computes the truth value of self_i >= other_i for each element of an array instance with the respective + element of the array other. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, Array] + Other array. Must be compatible with self (see Broadcasting). Should have a real-valued data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_ge) + + 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. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, bool, Array] + Other array. Must be compatible with self (see Broadcasting). May have any data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_eq) + + 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. + + Parameters + ---------- + self : Array + Array instance. Should have a numeric data type. + other: Union[int, float, bool, Array] + Other array. Must be compatible with self (see Broadcasting). May have any data type. + + Returns + ------- + out : Array + An array containing the element-wise results. The returned array must have a data type of bool. + """ + return _process_c_function(self, other, backend.get().af_neq) + + # Reflected Arithmetic Operators + + def __radd__(self, other: Array, /) -> Array: + """ + 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) + + # 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: 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: Union[int, float, Array], /) -> Array: + """ + Return self -= other. + """ + return _process_c_function(self, other, backend.get().af_sub) + + 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: Union[int, float, Array], /) -> Array: + """ + Return self /= other. + """ + return _process_c_function(self, other, backend.get().af_div) + + def __ifloordiv__(self, other: Union[int, float, Array], /) -> Array: + # TODO + return NotImplemented + + def __imod__(self, other: Union[int, float, Array], /) -> Array: + """ + Return self %= other. + """ + return _process_c_function(self, other, backend.get().af_mod) + + def __ipow__(self, other: Union[int, float, 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: Union[int, bool, Array], /) -> Array: + """ + Return self &= other. + """ + return _process_c_function(self, other, backend.get().af_bitand) + + 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: Union[int, bool, Array], /) -> Array: + """ + Return self ^= other. + """ + return _process_c_function(self, other, backend.get().af_bitxor) + + 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: Union[int, Array], /) -> Array: + """ + Return self >>= other. + """ + return _process_c_function(self, other, backend.get().af_bitshiftr) + + # Methods + + def __abs__(self) -> Array: + # TODO + return NotImplemented + + def __array_namespace__(self, *, api_version: Optional[str] = None) -> Any: + # TODO + return NotImplemented + + def __bool__(self) -> bool: + # TODO consider using scalar() and is_scalar() + return NotImplemented + + def __complex__(self) -> complex: + # TODO + return NotImplemented + + def __dlpack__(self, *, stream: Union[None, int, Any] = None): # type: ignore[no-untyped-def] + # TODO implementation and expected return type -> PyCapsule + return NotImplemented + + def __dlpack_device__(self) -> Tuple[enum.Enum, int]: + # TODO + return NotImplemented + + def __float__(self) -> float: + # TODO + return NotImplemented + + def __getitem__(self, key: Union[int, slice, Tuple[Union[int, slice, ], ...], Array], /) -> Array: + """ + Returns self[key]. + + Parameters + ---------- + self : Array + Array instance. + key : Union[int, slice, Tuple[Union[int, slice, ], ...], Array] + Index key. + + Returns + ------- + out : Array + An array containing the accessed value(s). The returned array must have the same data type as self. + """ + # TODO + # API Specification - key: Union[int, slice, ellipsis, Tuple[Union[int, slice, ellipsis], ...], array]. + # consider using af.span to replace ellipsis during refactoring + out = Array() + ndims = self.ndim + + if isinstance(key, Array) and key == af_bool.c_api_value: + ndims = 1 + if count(key) == 0: + return out + + safe_call(backend.get().af_index_gen( + 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: + # NOTE not a part of the array-api spec + return self.shape[0] if self.shape else 0 + + def __setitem__( + self, key: Union[int, slice, Tuple[Union[int, slice, ], ...], Array], + value: Union[int, float, bool, Array], /) -> None: + # TODO + return NotImplemented # type: ignore[return-value] # FIXME + + def __str__(self) -> str: + # 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) + + 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) + + def to_device(self, device: Any, /, *, stream: Union[int, Any] = None) -> Array: + # TODO implementation and change device type from Any to Device + return NotImplemented + + # Attributes + + @property + def dtype(self) -> Dtype: + """ + Data type of the array elements. + + Returns + ------- + out : Dtype + Array data type. + """ + 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) -> Any: + # TODO + return NotImplemented + + @property + def mT(self) -> Array: + # TODO + return NotImplemented + + @property + def T(self) -> Array: + """ + Transpose of the array. + + Returns + ------- + out : Array + Two-dimensional array whose first and last dimensions (axes) are permuted in reverse order relative to + original array. The returned array must have the same data type as the original array. + + Note + ---- + - The array instance must be two-dimensional. If the array instance is not two-dimensional, an error + should be raised. + """ + if self.ndim < 2: + raise TypeError(f"Array should be at least 2-dimensional. Got {self.ndim}-dimensional array") + + # TODO add check if out.dtype == self.dtype + out = Array() + 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)) + return out.value + + @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 + + @property + def shape(self) -> ShapeType: + """ + Array dimensions. + + Returns + ------- + out : tuple[int, ...] + Array dimensions. + """ + # 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)) + return (d0.value, d1.value, d2.value, d3.value)[:self.ndim] # Skip passing None values + + 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 + + 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 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 [] + + 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: + # NOTE not a part of the array-api spec + 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) + # 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: Optional[ShapeType] = None) -> str: + return ( + "arrayfire.Array()\n" + f"Type: {dtype.typename}\n" + f"Dims: {str(dims) if dims else ''}") + + +def _get_cshape(shape: Optional[ShapeType], buffer_length: int) -> CShape: + if shape: + return CShape(*shape) + + if buffer_length != 0: + return CShape(buffer_length) + + raise RuntimeError("Shape and buffer length have invalid size to process them into C shape.") + + +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 _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: + return dtype + + raise TypeError("There is no supported dtype that matches passed dtype typecode.") + + +def _process_c_function( + lhs: Union[int, float, Array], rhs: Union[int, float, Array], + c_function: Any) -> Array: + out = Array() + + if isinstance(lhs, Array) and isinstance(rhs, Array): + lhs_array = lhs.arr + rhs_array = rhs.arr + + elif isinstance(lhs, Array) and isinstance(rhs, (int, float)): + 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)) 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(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 + + +def _implicit_dtype(value: Union[int, float], 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: Union[int, float], 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.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), # 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), # 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)) + + return out 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/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..905d1ba97 --- /dev/null +++ b/arrayfire/array_api/dtype_functions.py @@ -0,0 +1,30 @@ +from .array_object import Array +from .dtypes import Dtype + +# TODO implement functions + + +def astype(x: Array, dtype: Dtype, /, *, copy: bool = True) -> Array: + return NotImplemented + + +def can_cast(from_: Dtype | Array, to: Dtype, /) -> bool: + return NotImplemented + + +def finfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] + # NOTE expected return type -> finfo_object + return NotImplemented + + +def iinfo(type: Dtype | Array, /): # type: ignore[no-untyped-def] + # NOTE expected return type -> iinfo_object + return NotImplemented + + +def isdtype(dtype: Dtype, kind: Dtype | str | tuple[Dtype | str, ...]) -> bool: + return NotImplemented + + +def result_type(*arrays_and_dtypes: Dtype | Array) -> Dtype: + return NotImplemented diff --git a/arrayfire/array_api/dtypes.py b/arrayfire/array_api/dtypes.py new file mode 100644 index 000000000..0059bf9cf --- /dev/null +++ b/arrayfire/array_api/dtypes.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +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 = [ + 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)) diff --git a/arrayfire/array_api/pytest.ini b/arrayfire/array_api/pytest.ini new file mode 100644 index 000000000..7fd828bec --- /dev/null +++ b/arrayfire/array_api/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +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/__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_object.py b/arrayfire/array_api/tests/test_array_object.py new file mode 100644 index 000000000..f30567553 --- /dev/null +++ b/arrayfire/array_api/tests/test_array_object.py @@ -0,0 +1,459 @@ +import array as pyarray +import math +from typing import Any + +import pytest + +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 +# 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: + array = Array() + + assert array.dtype == float32 + assert array.ndim == 0 + assert array.size == 0 + assert array.shape == () + assert len(array) == 0 + + +def test_create_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_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 + assert array.ndim == 2 + assert array.size == math.prod(array.shape) == 6 + assert array.shape == (2, 3) + assert len(array) == 2 + + +def test_create_array_from_1d_list() -> None: + array = Array([1, 2, 3]) + + assert array.dtype == float32 + assert array.ndim == 1 + assert array.size == math.prod(array.shape) == 3 + assert array.shape == (3,) + assert len(array) == 3 + + +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 == math.prod(array.shape) == 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: + 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 == math.prod(array1.shape) == math.prod(array2.shape) == 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] + + +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_scalar() -> None: + array = Array([1, 2, 3]) + assert array[1].scalar() == 2 + + +def test_scalar_is_empty() -> None: + array = Array() + assert array.scalar() is None + + +def test_array_to_list() -> None: + array = Array([1, 2, 3]) + assert array.to_list() == [1, 2, 3] + + +def test_array_to_list_is_empty() -> None: + array = Array() + assert array.to_list() == [] + + +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 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): + self.array + self.const_str # type: ignore[operator] + + with pytest.raises(TypeError): + self.array + 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() == -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 + 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] + + 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] + + 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] + + 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] + + with pytest.raises(TypeError): + self.array % self.tuple # type: ignore[operator] diff --git a/arrayfire/array_api/utils.py b/arrayfire/array_api/utils.py new file mode 100644 index 000000000..779459efb --- /dev/null +++ b/arrayfire/array_api/utils.py @@ -0,0 +1,11 @@ +from .array_object import Array + +# TODO implement functions + + +def all(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: + return NotImplemented + + +def any(x: Array, /, *, axis: None | int | tuple[int, ...] = None, keepdims: bool = False) -> Array: + return NotImplemented diff --git a/arrayfire/blas.py b/arrayfire/blas.py index f0e9dfdc6..448261e90 100644 --- a/arrayfire/blas.py +++ b/arrayfire/blas.py @@ -202,3 +202,109 @@ def dot(lhs, rhs, lhs_opts=MATPROP.NONE, rhs_opts=MATPROP.NONE, return_scalar = safe_call(backend.get().af_dot(c_pointer(out.arr), lhs.arr, rhs.arr, lhs_opts.value, rhs_opts.value)) return out + +def gemm(lhs, rhs, alpha=1.0, beta=0.0, lhs_opts=MATPROP.NONE, rhs_opts=MATPROP.NONE, C=None): + """ + BLAS general matrix multiply (GEMM) of two af_array objects. + + This provides a general interface to the BLAS level 3 general matrix multiply (GEMM), which is generally defined as: + + C = alpha * opA(A) opB(B) + beta * C + + where alpha and beta are both scalars; A and B are the matrix multiply operands; + and opA and opB are noop (if AF_MAT_NONE) or transpose (if AF_MAT_TRANS) operations + on A or B before the actual GEMM operation. + Batched GEMM is supported if at least either A or B have more than two dimensions + (see af::matmul for more details on broadcasting). + However, only one alpha and one beta can be used for all of the batched matrix operands. + + Parameters + ---------- + + lhs : af.Array + A 2 dimensional, real or complex arrayfire array. + + rhs : af.Array + A 2 dimensional, real or complex arrayfire array. + + alpha : scalar + + beta : scalar + + lhs_opts: optional: af.MATPROP. default: af.MATPROP.NONE. + Can be one of + - af.MATPROP.NONE - If no op should be done on `lhs`. + - af.MATPROP.TRANS - If `lhs` has to be transposed before multiplying. + - af.MATPROP.CTRANS - If `lhs` has to be hermitian transposed before multiplying. + + rhs_opts: optional: af.MATPROP. default: af.MATPROP.NONE. + Can be one of + - af.MATPROP.NONE - If no op should be done on `rhs`. + - af.MATPROP.TRANS - If `rhs` has to be transposed before multiplying. + - af.MATPROP.CTRANS - If `rhs` has to be hermitian transposed before multiplying. + + Returns + ------- + + out : af.Array + Output of the matrix multiplication on `lhs` and `rhs`. + + Note + ----- + + - The data types of `lhs` and `rhs` should be the same. + - Batches are not supported. + + """ + if C is None: + out = Array() + else: + out = C + + ltype = lhs.dtype() + + if ltype == Dtype.f32: + aptr = c_cast(c_pointer(c_float_t(alpha)),c_void_ptr_t) + bptr = c_cast(c_pointer(c_float_t(beta)), c_void_ptr_t) + elif ltype == Dtype.c32: + if isinstance(alpha, af_cfloat_t): + aptr = c_cast(c_pointer(alpha), c_void_ptr_t) + elif isinstance(alpha, tuple): + aptr = c_cast(c_pointer(af_cfloat_t(alpha[0], alpha[1])), c_void_ptr_t) + else: + aptr = c_cast(c_pointer(af_cfloat_t(alpha)), c_void_ptr_t) + + if isinstance(beta, af_cfloat_t): + bptr = c_cast(c_pointer(beta), c_void_ptr_t) + elif isinstance(beta, tuple): + bptr = c_cast(c_pointer(af_cfloat_t(beta[0], beta[1])), c_void_ptr_t) + else: + bptr = c_cast(c_pointer(af_cfloat_t(beta)), c_void_ptr_t) + + elif ltype == Dtype.f64: + aptr = c_cast(c_pointer(c_double_t(alpha)),c_void_ptr_t) + bptr = c_cast(c_pointer(c_double_t(beta)), c_void_ptr_t) + elif ltype == Dtype.c64: + if isinstance(alpha, af_cdouble_t): + aptr = c_cast(c_pointer(alpha), c_void_ptr_t) + elif isinstance(alpha, tuple): + aptr = c_cast(c_pointer(af_cdouble_t(alpha[0], alpha[1])), c_void_ptr_t) + else: + aptr = c_cast(c_pointer(af_cdouble_t(alpha)), c_void_ptr_t) + + if isinstance(beta, af_cdouble_t): + bptr = c_cast(c_pointer(beta), c_void_ptr_t) + elif isinstance(beta, tuple): + bptr = c_cast(c_pointer(af_cdouble_t(beta[0], beta[1])), c_void_ptr_t) + else: + bptr = c_cast(c_pointer(af_cdouble_t(beta)), c_void_ptr_t) + elif ltype == Dtype.f16: + raise TypeError("fp16 currently unsupported gemm() input type") + else: + raise TypeError("unsupported input type") + + + safe_call(backend.get().af_gemm(c_pointer(out.arr), + lhs_opts.value, rhs_opts.value, + aptr, lhs.arr, rhs.arr, bptr)) + return out diff --git a/arrayfire/cuda.py b/arrayfire/cuda.py index ea24c0e30..e5ef819be 100644 --- a/arrayfire/cuda.py +++ b/arrayfire/cuda.py @@ -85,3 +85,10 @@ def set_native_id(idx): safe_call(backend.get().afcu_set_native_id(idx)) return + +def set_cublas_mode(mode=CUBLAS_MATH_MODE.DEFAULT): + """ + Set's cuBLAS math mode for CUDA backend. In other backends, this has no effect. + """ + safe_call(backend().get().afcu_cublasSetMathMode(mode.value)) + return diff --git a/arrayfire/data.py b/arrayfire/data.py index d9ad19e48..1fbe17a53 100644 --- a/arrayfire/data.py +++ b/arrayfire/data.py @@ -799,6 +799,58 @@ def replace(lhs, cond, rhs): else: safe_call(backend.get().af_replace_scalar(lhs.arr, cond.arr, c_double_t(rhs))) +def pad(a, beginPadding, endPadding, padFillType = PAD.ZERO): + """ + Pad an array + + This function will pad an array with the specified border size. + Newly padded values can be filled in several different ways. + + Parameters + ---------- + + a: af.Array + A multi dimensional input arrayfire array. + + beginPadding: tuple of ints. default: (0, 0, 0, 0). + + endPadding: tuple of ints. default: (0, 0, 0, 0). + + padFillType: optional af.PAD default: af.PAD.ZERO + specifies type of values to fill padded border with + + Returns + ------- + output: af.Array + A padded array + + Examples + --------- + >>> import arrayfire as af + >>> a = af.randu(3,3) + >>> af.display(a) + [3 3 1 1] + 0.4107 0.1794 0.3775 + 0.8224 0.4198 0.3027 + 0.9518 0.0081 0.6456 + + >>> padded = af.pad(a, (1, 1), (1, 1), af.ZERO) + >>> af.display(padded) + [5 5 1 1] + 0.0000 0.0000 0.0000 0.0000 0.0000 + 0.0000 0.4107 0.1794 0.3775 0.0000 + 0.0000 0.8224 0.4198 0.3027 0.0000 + 0.0000 0.9518 0.0081 0.6456 0.0000 + 0.0000 0.0000 0.0000 0.0000 0.0000 + """ + out = Array() + begin_dims = dim4(beginPadding[0], beginPadding[1], beginPadding[2], beginPadding[3]) + end_dims = dim4(endPadding[0], endPadding[1], endPadding[2], endPadding[3]) + + safe_call(backend.get().af_pad(c_pointer(out.arr), a.arr, 4, c_pointer(begin_dims), 4, c_pointer(end_dims), padFillType.value)) + return out + + def lookup(a, idx, dim=0): """ Lookup the values of input array based on index. diff --git a/arrayfire/device.py b/arrayfire/device.py index 84594f2b3..53f302db5 100644 --- a/arrayfire/device.py +++ b/arrayfire/device.py @@ -150,6 +150,25 @@ def is_dbl_supported(device=None): safe_call(backend.get().af_get_dbl_support(c_pointer(res), dev)) return res.value +def is_half_supported(device=None): + """ + Check if half precision is supported on specified device. + + Parameters + ----------- + device: optional: int. default: None. + id of the desired device. + + Returns + -------- + - True if half precision supported. + - False if half precision not supported. + """ + dev = device if device is not None else get_device() + res = c_bool_t(False) + safe_call(backend.get().af_get_half_support(c_pointer(res), dev)) + return res.value + def sync(device=None): """ Block until all the functions on the device have completed execution. diff --git a/arrayfire/graphics.py b/arrayfire/graphics.py index 4e378aaf5..70881f42c 100644 --- a/arrayfire/graphics.py +++ b/arrayfire/graphics.py @@ -496,6 +496,34 @@ def set_axes_limits(self, xmin, xmax, ymin, ymax, zmin=None, zmax=None, exact=Fa c_float_t(zmin), c_float_t(zmax), exact, c_pointer(_cell))) + def set_axes_label_format(self, xformat="4.1%f", yformat="4.1%f", zformat="4.1%f"): + """ + Set axis limits. + + Parameters + ---------- + + xformat : str. + default: "4.1%f". + is a printf-style format specifier for x-axis + yformat : str. + default: "4.1%f". + is a printf-style format specifier for y-axis + zformat : str. + default: "4.1%f". + is a printf-style format specifier for z-axis + + """ + _cell = _Cell(self._r, self._c, None, self._cmap) + xformat = xformat.encode("ascii") + yformat = yformat.encode("ascii") + zformat = zformat.encode("ascii") + safe_call(backend.get().af_set_axes_label_format(self._wnd, + c_char_ptr_t(xformat), + c_char_ptr_t(yformat), + c_char_ptr_t(zformat), + c_pointer(_cell))) + def __getitem__(self, keys): """ Get access to a specific grid location within the window. diff --git a/arrayfire/image.py b/arrayfire/image.py index 92c8d088b..f626a4802 100644 --- a/arrayfire/image.py +++ b/arrayfire/image.py @@ -711,6 +711,49 @@ def regions(image, conn = CONNECTIVITY.FOUR, out_type = Dtype.f32): conn.value, out_type.value)) return output +def confidenceCC(image, seedx, seedy, radius, multiplier, iters, segmented_value): + """ + Find the confidence connected components in the image. + + Parameters + ---------- + image : af.Array + - A 2 D arrayfire array representing an image. + Expects non-integral type + + seedx : af.Array + - An array with x-coordinates of seed points + + seedy : af.Array + - An array with y-coordinates of seed points + + radius : scalar + - The neighborhood region to be considered around + each seed point + + multiplier : scalar + - Controls the threshold range computed from + the mean and variance of seed point neighborhoods + + iters : scalar + - is number of iterations + + segmented_value : scalar + - the value to which output array valid + pixels are set to. + + Returns + --------- + + output : af.Array + - Output array with resulting connected components + + """ + output = Array() + safe_call(backend.get().af_confidence_cc(c_pointer(output.arr), image.arr, seedx.arr, seedy.arr, + c_uint_t(radius), c_uint_t(multiplier), c_int_t(iters), c_double_t(segmented_value))) + return output + def sobel_derivatives(image, w_len=3): """ Find the sobel derivatives of the image. @@ -1281,6 +1324,78 @@ def anisotropic_diffusion(image, time_step, conductance, iterations, flux_functi flux_function_type.value, diffusion_kind.value)) return out +def iterativeDeconv(image, psf, iterations, relax_factor, algo = ITERATIVE_DECONV.DEFAULT): + """ + Iterative deconvolution algorithm. + + Parameters + ---------- + image: af.Array + The blurred input image. + + psf: af.Array + The kernel(point spread function) known to have caused + the blur in the system. + + iterations: + Number of times the algorithm will run. + + relax_factor: scalar. + is the relaxation factor multiplied with distance + of estimate from observed image. + + algo: + takes enum value of type af.ITERATIVE_DECONV + indicating the iterative deconvolution algorithm to be used + + Returns + ------- + out: af.Array + sharp image estimate generated from the blurred input + + Note + ------- + relax_factor argument is ignored when the RICHARDSONLUCY algorithm is used. + + """ + out = Array() + safe_call(backend.get(). + af_iterative_deconv(c_pointer(out.arr), image.arr, psf.arr, + c_uint_t(iterations), c_float_t(relax_factor), algo.value)) + return out + +def inverseDeconv(image, psf, gamma, algo = ITERATIVE_DECONV.DEFAULT): + """ + Inverse deconvolution algorithm. + + Parameters + ---------- + image: af.Array + The blurred input image. + + psf: af.Array + The kernel(point spread function) known to have caused + the blur in the system. + + gamma: scalar. + is a user defined regularization constant + + algo: + takes enum value of type af.INVERSE_DECONV + indicating the inverse deconvolution algorithm to be used + + Returns + ------- + out: af.Array + sharp image estimate generated from the blurred input + + """ + out = Array() + safe_call(backend.get(). + af_inverse_deconv(c_pointer(out.arr), image.arr, psf.arr, + c_float_t(gamma), algo.value)) + return out + def is_image_io_available(): """ Function to check if the arrayfire library was built with Image IO support. diff --git a/arrayfire/lapack.py b/arrayfire/lapack.py index e6ffd5cb7..97ad92c7a 100644 --- a/arrayfire/lapack.py +++ b/arrayfire/lapack.py @@ -264,6 +264,39 @@ def inverse(A, options=MATPROP.NONE): safe_call(backend.get().af_inverse(c_pointer(AI.arr), A.arr, options.value)) return AI +def pinverse(A, tol=1E-6, options=MATPROP.NONE): + """ + Find pseudo-inverse(Moore-Penrose) of a matrix. + + Parameters + ---------- + + A: af.Array + - A 2 dimensional arrayfire input matrix array + + tol: optional: scalar. default: 1E-6. + - Tolerance for calculating rank + + options: optional: af.MATPROP. default: af.MATPROP.NONE. + - Currently needs to be `af.MATPROP.NONE`. + - Additional options may speed up computation in the future + + Returns + ------- + + AI: af.Array + - A 2 dimensional array that is the pseudo-inverse of `A` + + Note + ---- + + This function is not supported in GFOR + + """ + AI = Array() + safe_call(backend.get().af_pinverse(c_pointer(AI.arr), A.arr, c_double_t(tol), options.value)) + return AI + def rank(A, tol=1E-5): """ Rank of a matrix. diff --git a/arrayfire/library.py b/arrayfire/library.py index 0bdc2eeb9..df68f97d8 100644 --- a/arrayfire/library.py +++ b/arrayfire/library.py @@ -15,6 +15,7 @@ import ctypes as ct import traceback import os +import sys c_float_t = ct.c_float c_double_t = ct.c_double @@ -31,6 +32,13 @@ c_void_ptr_t = ct.c_void_p c_char_ptr_t = ct.c_char_p c_size_t = ct.c_size_t +c_cast = ct.cast + +class af_cfloat_t(ct.Structure): + _fields_ = [("real", ct.c_float), ("imag", ct.c_float)] + +class af_cdouble_t(ct.Structure): + _fields_ = [("real", ct.c_double), ("imag", ct.c_double)] AF_VER_MAJOR = '3' @@ -97,6 +105,7 @@ class ERR(_Enum): # 400-499 Errors for missing hardware features NO_DBL = _Enum_Type(401) NO_GFX = _Enum_Type(402) + NO_HALF = _Enum_Type(403) # 500-599 Errors specific to the heterogeneous API LOAD_LIB = _Enum_Type(501) @@ -123,6 +132,7 @@ class Dtype(_Enum): u64 = _Enum_Type(9) s16 = _Enum_Type(10) u16 = _Enum_Type(11) + f16 = _Enum_Type(12) class Source(_Enum): """ @@ -152,6 +162,8 @@ class PAD(_Enum): """ ZERO = _Enum_Type(0) SYM = _Enum_Type(1) + CLAMP_TO_EDGE = _Enum_Type(2) + PERIODIC = _Enum_Type(3) class CONNECTIVITY(_Enum): """ @@ -175,6 +187,15 @@ class CONV_DOMAIN(_Enum): SPATIAL = _Enum_Type(1) FREQ = _Enum_Type(2) +class CONV_GRADIENT(_Enum): + """ + Convolution gradient type + """ + DEFAULT = _Enum_Type(0) + FILTER = _Enum_Type(1) + DATA = _Enum_Type(2) + BIAS = _Enum_Type(3) + class MATCH(_Enum): """ Match type @@ -446,18 +467,45 @@ class TOPK(_Enum): MIN = _Enum_Type(1) MAX = _Enum_Type(2) +class ITERATIVE_DECONV(_Enum): + """ + Iterative deconvolution algorithm + """ + DEFAULT = _Enum_Type(0) + LANDWEBER = _Enum_Type(1) + RICHARDSONLUCY = _Enum_Type(2) + +class INVERSE_DECONV(_Enum): + """ + Inverse deconvolution algorithm + """ + DEFAULT = _Enum_Type(0) + TIKHONOV = _Enum_Type(1) + +class VARIANCE(_Enum): + """ + Variance bias type + """ + DEFAULT = _Enum_Type(0) + SAMPLE = _Enum_Type(1) + POPULATION = _Enum_Type(2) + +class CUBLAS_MATH_MODE(_Enum): + """ + Enable Tensor Core usage if available on CUDA backend GPUs + """ + DEFAULT = _Enum_Type(0) + TENSOR_OP = _Enum_Type(1) + _VER_MAJOR_PLACEHOLDER = "__VER_MAJOR__" def _setup(): - import platform - platform_name = platform.system() try: AF_PATH = os.environ['AF_PATH'] except KeyError: AF_PATH = None - pass AF_SEARCH_PATH = AF_PATH @@ -465,7 +513,6 @@ def _setup(): CUDA_PATH = os.environ['CUDA_PATH'] except KeyError: CUDA_PATH= None - pass CUDA_FOUND = False @@ -484,7 +531,7 @@ def _setup(): ct.windll.kernel32.SetErrorMode(0x0001 | 0x0002) if AF_SEARCH_PATH is None: - AF_SEARCH_PATH="C:/Program Files/ArrayFire/v" + AF_VER_MAJOR +"/" + AF_SEARCH_PATH = "C:/Program Files/ArrayFire/v" + AF_VER_MAJOR +"/" if CUDA_PATH is not None: CUDA_FOUND = os.path.isdir(CUDA_PATH + '/bin') and os.path.isdir(CUDA_PATH + '/nvvm/bin/') @@ -496,7 +543,10 @@ def _setup(): post = '.' + _VER_MAJOR_PLACEHOLDER + '.dylib' if AF_SEARCH_PATH is None: - AF_SEARCH_PATH='/usr/local/' + if os.path.exists('/opt/arrayfire'): + AF_SEARCH_PATH = '/opt/arrayfire/' + else: + AF_SEARCH_PATH = '/usr/local/' if CUDA_PATH is None: CUDA_PATH='/usr/local/cuda/' @@ -508,7 +558,12 @@ def _setup(): post = '.so.' + _VER_MAJOR_PLACEHOLDER if AF_SEARCH_PATH is None: - AF_SEARCH_PATH='/opt/arrayfire-' + AF_VER_MAJOR + '/' + if os.path.exists('/opt/arrayfire-' + AF_VER_MAJOR + '/'): + AF_SEARCH_PATH = '/opt/arrayfire-' + AF_VER_MAJOR + '/' + elif os.path.exists('/opt/arrayfire/'): + AF_SEARCH_PATH = '/opt/arrayfire/' + else: + AF_SEARCH_PATH = '/usr/local/' if CUDA_PATH is None: CUDA_PATH='/usr/local/cuda/' @@ -520,18 +575,46 @@ def _setup(): else: raise OSError(platform_name + ' not supported') - if AF_PATH is None: - os.environ['AF_PATH'] = AF_SEARCH_PATH - - return pre, post, AF_SEARCH_PATH, CUDA_FOUND + return pre, post, AF_PATH, AF_SEARCH_PATH, CUDA_FOUND class _clibrary(object): + def __find_nvrtc_builtins_libname(self, search_path): + filelist = os.listdir(search_path) + for f in filelist: + if 'nvrtc-builtins' in f: + return f + return None + def __libname(self, name, head='af', ver_major=AF_VER_MAJOR): post = self.__post.replace(_VER_MAJOR_PLACEHOLDER, ver_major) libname = self.__pre + head + name + post - libname_full = self.AF_PATH + '/lib/' + libname - return (libname, libname_full) + + if self.AF_PATH: + if os.path.isdir(self.AF_PATH + '/lib64'): + path_search = self.AF_PATH + '/lib64/' + else: + path_search = self.AF_PATH + '/lib/' + else: + if os.path.isdir(self.AF_SEARCH_PATH + '/lib64'): + path_search = self.AF_SEARCH_PATH + '/lib64/' + else: + path_search = self.AF_SEARCH_PATH + '/lib/' + + if platform.architecture()[0][:2] == '64': + path_site = sys.prefix + '/lib64/' + else: + path_site = sys.prefix + '/lib/' + + path_local = self.AF_PYMODULE_PATH + libpaths = [('', libname), + (path_site, libname), + (path_local,libname)] + if self.AF_PATH: #prefer specified AF_PATH if exists + libpaths.append((path_search, libname)) + else: + libpaths.insert(2, (path_search, libname)) + return libpaths def set_unsafe(self, name): lib = self.__clibs[name] @@ -543,13 +626,19 @@ def __init__(self): more_info_str = "Please look at https://github.com/arrayfire/arrayfire-python/wiki for more information." - pre, post, AF_PATH, CUDA_FOUND = _setup() + pre, post, AF_PATH, AF_SEARCH_PATH, CUDA_FOUND = _setup() self.__pre = pre self.__post = post self.AF_PATH = AF_PATH + self.AF_SEARCH_PATH = AF_SEARCH_PATH self.CUDA_FOUND = CUDA_FOUND + # prefer locally packaged arrayfire libraries if they exist + af_module = __import__(__name__) + self.AF_PYMODULE_PATH = af_module.__path__[0] + '/' if af_module.__path__ else None + + self.__name = None self.__clibs = {'cuda' : None, @@ -569,25 +658,24 @@ def __init__(self): 'opencl' : 4} # Try to pre-load forge library if it exists - libnames = self.__libname('forge', head='', ver_major=FORGE_VER_MAJOR) + libnames = reversed(self.__libname('forge', head='', ver_major=FORGE_VER_MAJOR)) try: VERBOSE_LOADS = os.environ['AF_VERBOSE_LOADS'] == '1' except KeyError: VERBOSE_LOADS = False - pass for libname in libnames: try: - ct.cdll.LoadLibrary(libname) + full_libname = libname[0] + libname[1] + ct.cdll.LoadLibrary(full_libname) if VERBOSE_LOADS: - print('Loaded ' + libname) + print('Loaded ' + full_libname) break except OSError: if VERBOSE_LOADS: traceback.print_exc() - print('Unable to load ' + libname) - pass + print('Unable to load ' + full_libname) c_dim4 = c_dim_t*4 out = c_void_ptr_t(0) @@ -595,25 +683,39 @@ def __init__(self): # Iterate in reverse order of preference for name in ('cpu', 'opencl', 'cuda', ''): - libnames = self.__libname(name) + libnames = reversed(self.__libname(name)) for libname in libnames: try: - ct.cdll.LoadLibrary(libname) + full_libname = libname[0] + libname[1] + + ct.cdll.LoadLibrary(full_libname) __name = 'unified' if name == '' else name - clib = ct.CDLL(libname) + clib = ct.CDLL(full_libname) self.__clibs[__name] = clib err = clib.af_randu(c_pointer(out), 4, c_pointer(dims), Dtype.f32.value) if (err == ERR.NONE.value): self.__name = __name clib.af_release_array(out) if VERBOSE_LOADS: - print('Loaded ' + libname) + print('Loaded ' + full_libname) + + # load nvrtc-builtins library if using cuda + if name == 'cuda': + nvrtc_name = self.__find_nvrtc_builtins_libname(libname[0]) + if nvrtc_name: + ct.cdll.LoadLibrary(libname[0] + nvrtc_name) + + if VERBOSE_LOADS: + print('Loaded ' + libname[0] + nvrtc_name) + else: + if VERBOSE_LOADS: + print('Could not find local nvrtc-builtins libarary') + break; except OSError: if VERBOSE_LOADS: traceback.print_exc() - print('Unable to load ' + libname) - pass + print('Unable to load ' + full_libname) if (self.__name is None): raise RuntimeError("Could not load any ArrayFire libraries.\n" + more_info_str) @@ -640,6 +742,7 @@ def parse(self, res): lst.append(key) return tuple(lst) + backend = _clibrary() def set_backend(name, unsafe=False): diff --git a/arrayfire/ml.py b/arrayfire/ml.py new file mode 100644 index 000000000..7e0fc53de --- /dev/null +++ b/arrayfire/ml.py @@ -0,0 +1,83 @@ +####################################################### +# Copyright (c) 2020, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +""" +Machine learning functions + - Pool 2D, ND, maxpooling, minpooling, meanpooling + - Forward and backward convolution passes +""" + +from .library import * +from .array import * + +def convolve2GradientNN(incoming_gradient, original_signal, original_kernel, convolved_output, stride = (1, 1), padding = (0, 0), dilation = (1, 1), gradType = CONV_GRADIENT.DEFAULT): + """ + Function for calculating backward pass gradient of 2D convolution. + + This function calculates the gradient with respect to the output of the + \ref convolve2NN() function that uses the machine learning formulation + for the dimensions of the signals and filters + + Multiple signals and filters can be batched against each other, however + their dimensions must match. + + Example: + Signals with dimensions: d0 x d1 x d2 x Ns + Filters with dimensions: d0 x d1 x d2 x Nf + + Resulting Convolution: d0 x d1 x Nf x Ns + + Parameters + ----------- + + incoming_gradient: af.Array + - Gradients to be distributed in backwards pass + + original_signal: af.Array + - A 2 dimensional signal or batch of 2 dimensional signals. + + original_kernel: af.Array + - A 2 dimensional kernel or batch of 2 dimensional kernels. + + convolved_output: af.Array + - output of forward pass of convolution + + stride: tuple of ints. default: (1, 1). + - Specifies how much to stride along each dimension + + padding: tuple of ints. default: (0, 0). + - Specifies signal padding along each dimension + + dilation: tuple of ints. default: (1, 1). + - Specifies how much to dilate kernel along each dimension before convolution + + Returns + -------- + + output: af.Array + - Gradient wrt/requested gradient type + + """ + output = Array() + stride_dim = dim4(stride[0], stride[1]) + padding_dim = dim4(padding[0], padding[1]) + dilation_dim = dim4(dilation[0], dilation[1]) + + safe_call(backend.get().af_convolve2_gradient_nn( + c_pointer(output.arr), + incoming_gradient.arr, + original_signal.arr, + original_kernel.arr, + convolved_output.arr, + 2, c_pointer(stride_dim), + 2, c_pointer(padding_dim), + 2, c_pointer(dilation_dim), + gradType.value)) + return output + diff --git a/arrayfire/signal.py b/arrayfire/signal.py index ca27d21c2..35e0fba87 100644 --- a/arrayfire/signal.py +++ b/arrayfire/signal.py @@ -27,7 +27,7 @@ def _scale_pos_axis1(y_curr, y_orig): dy = y_orig[0, 1, 0, 0] - y0 return((y_curr - y0) / dy) -def approx1(signal, x, method=INTERP.LINEAR, off_grid=0.0, xp = None): +def approx1(signal, x, method=INTERP.LINEAR, off_grid=0.0, xp = None, output = None): """ Interpolate along a single dimension.Interpolation is performed along axis 0 of the input array. @@ -51,6 +51,10 @@ def approx1(signal, x, method=INTERP.LINEAR, off_grid=0.0, xp = None): xp : af.Array The x-coordinates of the input data points + output: None or af.Array + Optional preallocated output array. If it is a sub-array of an existing af_array, + only the corresponding portion of the af_array will be overwritten + Returns ------- @@ -65,20 +69,86 @@ def approx1(signal, x, method=INTERP.LINEAR, off_grid=0.0, xp = None): where N is the length of the first dimension of `signal`. """ - output = Array() + if output is None: + output = Array() + + if(xp is not None): + pos0 = _scale_pos_axis0(x, xp) + else: + pos0 = x + + safe_call(backend.get().af_approx1(c_pointer(output.arr), signal.arr, pos0.arr, + method.value, c_float_t(off_grid))) - if(xp is not None): - pos0 = _scale_pos_axis0(x, xp) else: - pos0 = x + if(xp is not None): + pos0 = _scale_pos_axis0(x, xp) + else: + pos0 = x + safe_call(backend.get().af_approx1_v2(c_pointer(output.arr), signal.arr, pos0.arr, + method.value, c_float_t(off_grid))) + return output + + +def approx1_uniform(signal, x, interp_dim, idx_start, idx_step, method=INTERP.LINEAR, off_grid=0.0, output = None): + """ + Interpolation on one dimensional signals along specified dimension. + + af_approx1_uniform() accepts the dimension to perform the interpolation along the input. + It also accepts start and step values which define the uniform range of corresponding indices. + + Parameters + ---------- + + signal: af.Array + Input signal array (signal = f(x)) + + x: af.Array + The x-coordinates of the interpolation points. The interpolation + function is queried at these set of points. + + interp_dim: scalar + is the dimension to perform interpolation across. + + idx_start: scalar + is the first index value along interp_dim. + + idx_step: scalar + is the uniform spacing value between subsequent indices along interp_dim. + + method: optional: af.INTERP. default: af.INTERP.LINEAR. + Interpolation method. + + off_grid: optional: scalar. default: 0.0. + The value used for positions outside the range. + + output: None or af.Array + Optional preallocated output array. If it is a sub-array of an existing af_array, + only the corresponding portion of the af_array will be overwritten - safe_call(backend.get().af_approx1(c_pointer(output.arr), signal.arr, pos0.arr, - method.value, c_float_t(off_grid))) + Returns + ------- + + output: af.Array + Values calculated at interpolation points. + + """ + + if output is None: + output = Array() + + safe_call(backend.get().af_approx1_uniform(c_pointer(output.arr), signal.arr, x.arr, + c_dim_t(interp_dim), c_double_t(idx_start), c_double_t(idx_step), + method.value, c_float_t(off_grid))) + else: + safe_call(backend.get().af_approx1_uniform_v2(c_pointer(output.arr), signal.arr, x.arr, + c_dim_t(interp_dim), c_double_t(idx_start), c_double_t(idx_step), + method.value, c_float_t(off_grid))) return output + def approx2(signal, x, y, - method=INTERP.LINEAR, off_grid=0.0, xp = None, yp = None - ): + method=INTERP.LINEAR, off_grid=0.0, xp = None, yp = None, output = None): """ Interpolate along a two dimension.Interpolation is performed along axes 0 and 1 of the input array. @@ -112,6 +182,10 @@ def approx2(signal, x, y, The y-coordinates of the input data points. The convention followed is that the y-coordinates vary along axis 1 + output: None or af.Array + Optional preallocated output array. If it is a sub-array of an existing af_array, + only the corresponding portion of the af_array will be overwritten + Returns ------- @@ -127,22 +201,114 @@ def approx2(signal, x, y, and N is the length of the second dimension of `signal`. """ - output = Array() - - if(xp is not None): - pos0 = _scale_pos_axis0(x, xp) + if output is None: + output = Array() + + if(xp is not None): + pos0 = _scale_pos_axis0(x, xp) + else: + pos0 = x + + if(yp is not None): + pos1 = _scale_pos_axis1(y, yp) + else: + pos1 = y + + safe_call(backend.get().af_approx2(c_pointer(output.arr), signal.arr, + pos0.arr, pos1.arr, method.value, c_float_t(off_grid))) else: - pos0 = x + if(xp is not None): + pos0 = _scale_pos_axis0(x, xp) + else: + pos0 = x + + if(yp is not None): + pos1 = _scale_pos_axis1(y, yp) + else: + pos1 = y + + safe_call(backend.get().af_approx2_v2(c_pointer(output.arr), signal.arr, + pos0.arr, pos1.arr, method.value, c_float_t(off_grid))) + + return output + +def approx2_uniform(signal, pos0, interp_dim0, idx_start0, idx_step0, pos1, interp_dim1, idx_start1, idx_step1, + method=INTERP.LINEAR, off_grid=0.0, output = None): + """ + Interpolate along two uniformly spaced dimensions of the input array. + af_approx2_uniform() accepts two dimensions to perform the interpolation along the input. + It also accepts start and step values which define the uniform range of corresponding indices. + + Parameters + ---------- + + signal: af.Array + Input signal array (signal = f(x, y)) - if(yp is not None): - pos1 = _scale_pos_axis1(y, yp) + pos0 : af.Array + positions of the interpolation points along interp_dim0. + + interp_dim0: scalar + is the first dimension to perform interpolation across. + + idx_start0: scalar + is the first index value along interp_dim0. + + idx_step0: scalar + is the uniform spacing value between subsequent indices along interp_dim0. + + pos1 : af.Array + positions of the interpolation points along interp_dim1. + + interp_dim1: scalar + is the second dimension to perform interpolation across. + + idx_start1: scalar + is the first index value along interp_dim1. + + idx_step1: scalar + is the uniform spacing value between subsequent indices along interp_dim1. + + method: optional: af.INTERP. default: af.INTERP.LINEAR. + Interpolation method. + + off_grid: optional: scalar. default: 0.0. + The value used for positions outside the range. + + output: None or af.Array + Optional preallocated output array. If it is a sub-array of an existing af_array, + only the corresponding portion of the af_array will be overwritten + + Returns + ------- + + output: af.Array + Values calculated at interpolation points. + + Note + ----- + This holds applicable when x_input/y_input isn't provided: + + The initial measurements are assumed to have taken place at equal steps between [(0,0) - [M - 1, N - 1]] + where M is the length of the first dimension of `signal`, + and N is the length of the second dimension of `signal`. + """ + + if output is None: + output = Array() + safe_call(backend.get().af_approx2_uniform(c_pointer(output.arr), signal.arr, + pos0.arr, c_dim_t(interp_dim0), c_double_t(idx_start0), c_double_t(idx_step0), + pos1.arr, c_dim_t(interp_dim1), c_double_t(idx_start1), c_double_t(idx_step1), + method.value, c_float_t(off_grid))) else: - pos1 = y + safe_call(backend.get().af_approx2_uniform_v2(c_pointer(output.arr), signal.arr, + pos0.arr, c_dim_t(interp_dim0), c_double_t(idx_start0), c_double_t(idx_step0), + pos1.arr, c_dim_t(interp_dim1), c_double_t(idx_start1), c_double_t(idx_step1), + method.value, c_float_t(off_grid))) - safe_call(backend.get().af_approx2(c_pointer(output.arr), signal.arr, - pos0.arr, pos1.arr, method.value, c_float_t(off_grid))) return output + def fft(signal, dim0 = None , scale = None): """ Fast Fourier Transform: 1D @@ -972,6 +1138,56 @@ def convolve2(signal, kernel, conv_mode = CONV_MODE.DEFAULT, conv_domain = CONV_ conv_mode.value, conv_domain.value)) return output +def convolve2NN(signal, kernel, stride = (1, 1), padding = (0, 0), dilation = (1, 1)): + """ + This version of convolution is consistent with the machine learning + formulation that will spatially convolve a filter on 2-dimensions against a + signal. Multiple signals and filters can be batched against each other. + Furthermore, the signals and filters can be multi-dimensional however their + dimensions must match. + + Example: + Signals with dimensions: d0 x d1 x d2 x Ns + Filters with dimensions: d0 x d1 x d2 x Nf + + Resulting Convolution: d0 x d1 x Nf x Ns + + Parameters + ----------- + + signal: af.Array + - A 2 dimensional signal or batch of 2 dimensional signals. + + kernel: af.Array + - A 2 dimensional kernel or batch of 2 dimensional kernels. + + stride: tuple of ints. default: (1, 1). + - Specifies how much to stride along each dimension + + padding: tuple of ints. default: (0, 0). + - Specifies signal padding along each dimension + + dilation: tuple of ints. default: (1, 1). + - Specifies how much to dilate kernel along each dimension before convolution + + Returns + -------- + + output: af.Array + - Convolved 2D array. + + """ + output = Array() + stride_dim = dim4(stride[0], stride[1]) + padding_dim = dim4(padding[0], padding[1]) + dilation_dim = dim4(dilation[0], dilation[1]) + + safe_call(backend.get().af_convolve2_nn(c_pointer(output.arr), signal.arr, kernel.arr, + 2, c_pointer(stride_dim), + 2, c_pointer(padding_dim), + 2, c_pointer(dilation_dim))) + return output + def convolve2_separable(col_kernel, row_kernel, signal, conv_mode = CONV_MODE.DEFAULT): """ Convolution: 2D separable convolution diff --git a/arrayfire/sparse.py b/arrayfire/sparse.py index b261bdbd8..91b8f7a23 100644 --- a/arrayfire/sparse.py +++ b/arrayfire/sparse.py @@ -90,7 +90,10 @@ def create_sparse_from_host(values, row_idx, col_idx, nrows, ncols, storage = ST A sparse matrix. """ - return create_sparse(to_array(values), to_array(row_idx), to_array(col_idx), nrows, ncols, storage) + return create_sparse(to_array(values), + to_array(row_idx).as_type(Dtype.s32), + to_array(col_idx).as_type(Dtype.s32), + nrows, ncols, storage) def create_sparse_from_dense(dense, storage = STORAGE.CSR): """ diff --git a/arrayfire/statistics.py b/arrayfire/statistics.py index e6b4effd0..158da18de 100644 --- a/arrayfire/statistics.py +++ b/arrayfire/statistics.py @@ -59,7 +59,7 @@ def mean(a, weights=None, dim=None): return real if imag == 0 else real + imag * 1j -def var(a, isbiased=False, weights=None, dim=None): +def var(a, bias=VARIANCE.DEFAULT, weights=None, dim=None): """ Calculate variance along a given dimension. @@ -68,9 +68,9 @@ def var(a, isbiased=False, weights=None, dim=None): a: af.Array The input array. - isbiased: optional: Boolean. default: False. - Boolean denoting population variance (false) or sample - variance (true). + bias: optional: af.VARIANCE. default: DEFAULT. + population variance(VARIANCE.POPULATION) or sample variance(VARIANCE.SAMPLE). + This is ignored if weights are provided. weights: optional: af.Array. default: None. Array to calculate for the weighted mean. Must match size of @@ -89,7 +89,7 @@ def var(a, isbiased=False, weights=None, dim=None): out = Array() if weights is None: - safe_call(backend.get().af_var(c_pointer(out.arr), a.arr, isbiased, c_int_t(dim))) + safe_call(backend.get().af_var_v2(c_pointer(out.arr), a.arr, bias.value, c_int_t(dim))) else: safe_call(backend.get().af_var_weighted(c_pointer(out.arr), a.arr, weights.arr, c_int_t(dim))) @@ -99,7 +99,7 @@ def var(a, isbiased=False, weights=None, dim=None): imag = c_double_t(0) if weights is None: - safe_call(backend.get().af_var_all(c_pointer(real), c_pointer(imag), a.arr, isbiased)) + safe_call(backend.get().af_var_all_v2(c_pointer(real), c_pointer(imag), a.arr, bias.value)) else: safe_call(backend.get().af_var_all_weighted(c_pointer(real), c_pointer(imag), a.arr, weights.arr)) @@ -108,7 +108,49 @@ def var(a, isbiased=False, weights=None, dim=None): return real if imag == 0 else real + imag * 1j -def stdev(a, dim=None): +def meanvar(a, weights=None, bias=VARIANCE.DEFAULT, dim=-1): + """ + Calculate mean and variance along a given dimension. + + Parameters + ---------- + a: af.Array + The input array. + + weights: optional: af.Array. default: None. + Array to calculate for the weighted mean. Must match size of + the input array. + + bias: optional: af.VARIANCE. default: DEFAULT. + population variance(VARIANCE.POPULATION) or + sample variance(VARIANCE.SAMPLE). + + dim: optional: int. default: -1. + The dimension for which to obtain the variance from input data. + + Returns + ------- + mean: af.Array + Array containing the mean of the input array along a given + dimension. + variance: af.Array + Array containing the variance of the input array along a given + dimension. + """ + + mean_out = Array() + var_out = Array() + + if weights is None: + weights = Array() + + safe_call(backend.get().af_meanvar(c_pointer(mean_out.arr), c_pointer(var_out.arr), + a.arr, weights.arr, bias.value, c_int_t(dim))) + + return mean_out, var_out + + +def stdev(a, bias=VARIANCE.DEFAULT, dim=None): """ Calculate standard deviation along a given dimension. @@ -117,6 +159,10 @@ def stdev(a, dim=None): a: af.Array The input array. + bias: optional: af.VARIANCE. default: DEFAULT. + population variance(VARIANCE.POPULATION) or sample variance(VARIANCE.SAMPLE). + This is ignored if weights are provided. + dim: optional: int. default: None. The dimension for which to obtain the standard deviation from input data. @@ -129,48 +175,41 @@ def stdev(a, dim=None): """ if dim is not None: out = Array() - safe_call(backend.get().af_stdev(c_pointer(out.arr), a.arr, c_int_t(dim))) + safe_call(backend.get().af_stdev_v2(c_pointer(out.arr), a.arr, bias.value, + c_int_t(dim))) return out else: real = c_double_t(0) imag = c_double_t(0) - safe_call(backend.get().af_stdev_all(c_pointer(real), c_pointer(imag), a.arr)) + safe_call(backend.get().af_stdev_all_v2(c_pointer(real), c_pointer(imag), a.arr, + bias.value)) real = real.value imag = imag.value return real if imag == 0 else real + imag * 1j -def cov(a, isbiased=False, dim=None): +def cov(a, b, bias=VARIANCE.DEFAULT): """ Calculate covariance along a given dimension. Parameters ---------- a: af.Array - The input array. + Input array. - isbiased: optional: Boolean. default: False. - Boolean denoting whether biased estimate should be taken. + b: af.Array + Input array. - dim: optional: int. default: None. - The dimension for which to obtain the covariance from input data. + bias: optional: af.VARIANCE. default: DEFAULT. + population variance(VARIANCE.POPULATION) or sample variance(VARIANCE.SAMPLE). Returns ------- output: af.Array - Array containing the covariance of the input array along a - given dimension. + Array containing the covariance of the input array along a given dimension. """ - if dim is not None: - out = Array() - safe_call(backend.get().af_cov(c_pointer(out.arr), a.arr, isbiased, c_int_t(dim))) - return out - else: - real = c_double_t(0) - imag = c_double_t(0) - safe_call(backend.get().af_cov_all(c_pointer(real), c_pointer(imag), a.arr, isbiased)) - real = real.value - imag = imag.value - return real if imag == 0 else real + imag * 1j + out = Array() + safe_call(backend.get().af_cov_v2(c_pointer(out.arr), a.arr, b.arr, bias.value)) + return out def median(a, dim=None): """ diff --git a/arrayfire/tests/simple/__init__.py b/arrayfire/tests/simple/__init__.py deleted file mode 100644 index 26ed88961..000000000 --- a/arrayfire/tests/simple/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -####################################################### -# Copyright (c) 2015, ArrayFire -# All rights reserved. -# -# This file is distributed under 3-clause BSD license. -# The complete license agreement can be obtained at: -# http://arrayfire.com/licenses/BSD-3-Clause -######################################################## - -from .algorithm import * -from .arith import * -from .array_test import * -from .blas import * -from .data import * -from .device import * -from .image import * -from .index import * -from .interop import * -from .lapack import * -from .signal import * -from .statistics import * -from .random import * -from .sparse import * -from ._util import tests diff --git a/arrayfire/tests/simple_tests.py b/arrayfire/tests/simple_tests.py deleted file mode 100755 index 603ae9f37..000000000 --- a/arrayfire/tests/simple_tests.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python - -####################################################### -# Copyright (c) 2015, ArrayFire -# All rights reserved. -# -# This file is distributed under 3-clause BSD license. -# The complete license agreement can be obtained at: -# http://arrayfire.com/licenses/BSD-3-Clause -######################################################## - -from __future__ import absolute_import - -from . import simple -import sys - -if __name__ == "__main__": - verbose = False - - if len(sys.argv) > 1: - verbose = int(sys.argv[1]) - - test_list = None - if len(sys.argv) > 2: - test_list = sys.argv[2:] - - simple.tests.run(test_list, verbose) diff --git a/arrayfire/util.py b/arrayfire/util.py index 709bd7811..44af6000d 100644 --- a/arrayfire/util.py +++ b/arrayfire/util.py @@ -105,7 +105,8 @@ def get_reversion(): 'l' : Dtype.s64, 'L' : Dtype.u64, 'F' : Dtype.c32, - 'D' : Dtype.c64} + 'D' : Dtype.c64, + 'hf': Dtype.f16} to_typecode = {Dtype.f32.value : 'f', Dtype.f64.value : 'd', @@ -118,7 +119,8 @@ def get_reversion(): Dtype.s64.value : 'l', Dtype.u64.value : 'L', Dtype.c32.value : 'F', - Dtype.c64.value : 'D'} + Dtype.c64.value : 'D', + Dtype.f16.value : 'hf'} to_c_type = {Dtype.f32.value : c_float_t, Dtype.f64.value : c_double_t, @@ -131,7 +133,8 @@ def get_reversion(): Dtype.s64.value : c_longlong_t, Dtype.u64.value : c_ulonglong_t, Dtype.c32.value : c_float_t * 2, - Dtype.c64.value : c_double_t * 2} + Dtype.c64.value : c_double_t * 2, + Dtype.f16.value : c_ushort_t} to_typename = {Dtype.f32.value : 'float', Dtype.f64.value : 'double', @@ -144,4 +147,5 @@ def get_reversion(): Dtype.s64.value : 'long int', Dtype.u64.value : 'unsigned long int', Dtype.c32.value : 'float complex', - Dtype.c64.value : 'double complex'} + Dtype.c64.value : 'double complex', + Dtype.f16.value : 'half'} diff --git a/assets b/assets new file mode 160000 index 000000000..cd08d7496 --- /dev/null +++ b/assets @@ -0,0 +1 @@ +Subproject commit cd08d749611b324012555ad6f23fd76c5465bd6c diff --git a/docs/arrayfire.ml.rst b/docs/arrayfire.ml.rst new file mode 100644 index 000000000..0c53c1719 --- /dev/null +++ b/docs/arrayfire.ml.rst @@ -0,0 +1,7 @@ +arrayfire.ml module +======================= + +.. automodule:: arrayfire.ml + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/arrayfire.rst b/docs/arrayfire.rst index 2951fcb3b..7504fd82b 100644 --- a/docs/arrayfire.rst +++ b/docs/arrayfire.rst @@ -26,6 +26,7 @@ Submodules arrayfire.interop arrayfire.lapack arrayfire.library + arrayfire.ml arrayfire.opencl arrayfire.random arrayfire.sparse diff --git a/docs/conf.py b/docs/conf.py index 04937888d..44374afb1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,8 +58,8 @@ # General information about the project. project = 'ArrayFire' -copyright = '2016, Pavan Yalamanchili' -author = 'Pavan Yalamanchili' +copyright = '2020, ArrayFire' +author = 'Stefan Yurkevitch, Pradeep Garigipati, Umar Arshad' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/examples/benchmarks/bench_cg.py b/examples/benchmarks/bench_cg.py index 8a74f25a9..33c5b856c 100644 --- a/examples/benchmarks/bench_cg.py +++ b/examples/benchmarks/bench_cg.py @@ -80,6 +80,7 @@ def calc_arrayfire(A, b, x0, maxiter=10): beta_num = af.dot(r, r) beta = beta_num/alpha_num p = r + af.tile(beta, p.dims()[0]) * p + af.eval(x) res = x0 - x return x, af.dot(res, res) @@ -137,11 +138,11 @@ def timeit(calc, iters, args): def test(): print("\nTesting benchmark functions...") - A, b, x0 = setup_input(50) # dense A + A, b, x0 = setup_input(n=50, sparsity=7) # dense A Asp = to_sparse(A) x1, _ = calc_arrayfire(A, b, x0) x2, _ = calc_arrayfire(Asp, b, x0) - if af.sum(af.abs(x1 - x2)/x2 > 1e-6): + if af.sum(af.abs(x1 - x2)/x2 > 1e-5): raise ValueError("arrayfire test failed") if np: An = to_numpy(A) @@ -162,11 +163,13 @@ def test(): def bench(n=4*1024, sparsity=7, maxiter=10, iters=10): + # generate data print("\nGenerating benchmark data for n = %i ..." %n) A, b, x0 = setup_input(n, sparsity) # dense A Asp = to_sparse(A) # sparse A input_info(A, Asp) + # make benchmarks print("Benchmarking CG solver for n = %i ..." %n) t1 = timeit(calc_arrayfire, iters, args=(A, b, x0, maxiter)) @@ -192,9 +195,8 @@ def bench(n=4*1024, sparsity=7, maxiter=10, iters=10): if (len(sys.argv) > 1): af.set_device(int(sys.argv[1])) - af.info() - + af.info() test() - + for n in (128, 256, 512, 1024, 2048, 4096): bench(n) diff --git a/examples/common/idxio.py b/examples/common/idxio.py new file mode 100644 index 000000000..8776c62dc --- /dev/null +++ b/examples/common/idxio.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2019, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +def reverse_char(b): + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4 + b = (b & 0xCC) >> 2 | (b & 0x33) << 2 + b = (b & 0xAA) >> 1 | (b & 0x55) << 1 + return b + + +# http://stackoverflow.com/a/9144870/2192361 +def reverse(x): + x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1) + x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2) + x = ((x >> 4) & 0x0f0f0f0f) | ((x & 0x0f0f0f0f) << 4) + x = ((x >> 8) & 0x00ff00ff) | ((x & 0x00ff00ff) << 8) + x = ((x >> 16) & 0xffff) | ((x & 0xffff) << 16); + return x + + +def read_idx(name): + with open(name, 'rb') as f: + # In the C++ version, bytes the size of 4 chars are being read + # May not work properly in machines where a char is not 1 byte + bytes_read = f.read(4) + bytes_read = bytearray(bytes_read) + + if bytes_read[2] != 8: + raise RuntimeError('Unsupported data type') + + numdims = bytes_read[3] + elemsize = 1 + + # Read the dimensions + elem = 1 + dims = [0] * numdims + for i in range(numdims): + bytes_read = bytearray(f.read(4)) + + # Big endian to little endian + for j in range(4): + bytes_read[j] = reverse_char(bytes_read[j]) + bytes_read_int = int.from_bytes(bytes_read, 'little') + dim = reverse(bytes_read_int) + + elem = elem * dim; + dims[i] = dim; + + # Read the data + cdata = f.read(elem * elemsize) + cdata = list(cdata) + data = [float(cdata_elem) for cdata_elem in cdata] + + return (dims, data) + + diff --git a/examples/computer_vision/fast.py b/examples/computer_vision/fast.py new file mode 100644 index 000000000..e3a1299e3 --- /dev/null +++ b/examples/computer_vision/fast.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2018, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from time import time +import arrayfire as af +import os +import sys + +def draw_corners(img, x, y, draw_len): + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + xmin = max(0, x - draw_len) + xmax = min(img.dims()[1], x + draw_len) + + img[y, xmin : xmax, 0] = 0.0 + img[y, xmin : xmax, 1] = 1.0 + img[y, xmin : xmax, 2] = 0.0 + + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + ymin = int(max(0, y - draw_len)) + ymax = int(min(img.dims()[0], y + draw_len)) + + img[ymin : ymax, x, 0] = 0.0 + img[ymin : ymax, x, 1] = 1.0 + img[ymin : ymax, x, 2] = 0.0 + return img + +def fast_demo(console): + + root_path = os.path.dirname(os.path.abspath(__file__)) + file_path = root_path + if console: + file_path += "/../../assets/examples/images/square.png" + else: + file_path += "/../../assets/examples/images/man.jpg" + img_color = af.load_image(file_path, True); + + img = af.color_space(img_color, af.CSPACE.GRAY, af.CSPACE.RGB) + img_color /= 255.0 + + features = af.fast(img) + + xs = features.get_xpos().to_list() + ys = features.get_ypos().to_list() + + draw_len = 3; + num_features = features.num_features().value + for f in range(num_features): + print(f) + x = xs[f] + y = ys[f] + + img_color = draw_corners(img_color, x, y, draw_len) + + + print("Features found: {}".format(num_features)) + if not console: + # Previews color image with green crosshairs + wnd = af.Window(512, 512, "FAST Feature Detector") + + while not wnd.close(): + wnd.image(img_color) + else: + print(xs); + print(ys); + + +if __name__ == "__main__": + if (len(sys.argv) > 1): + af.set_device(int(sys.argv[1])) + console = (sys.argv[2] == '-') if len(sys.argv) > 2 else False + + af.info() + print("** ArrayFire FAST Feature Detector Demo **\n") + fast_demo(console) + diff --git a/examples/computer_vision/harris.py b/examples/computer_vision/harris.py new file mode 100644 index 000000000..27fd6c6f8 --- /dev/null +++ b/examples/computer_vision/harris.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2018, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from time import time +import arrayfire as af +import os +import sys + +def draw_corners(img, x, y, draw_len): + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + xmin = max(0, x - draw_len) + xmax = min(img.dims()[1], x + draw_len) + + img[y, xmin : xmax, 0] = 0.0 + img[y, xmin : xmax, 1] = 1.0 + img[y, xmin : xmax, 2] = 0.0 + + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + ymin = max(0, y - draw_len) + ymax = min(img.dims()[0], y + draw_len) + + img[ymin : ymax, x, 0] = 0.0 + img[ymin : ymax, x, 1] = 1.0 + img[ymin : ymax, x, 2] = 0.0 + return img + +def harris_demo(console): + + root_path = os.path.dirname(os.path.abspath(__file__)) + file_path = root_path + if console: + file_path += "/../../assets/examples/images/square.png" + else: + file_path += "/../../assets/examples/images/man.jpg" + img_color = af.load_image(file_path, True); + + img = af.color_space(img_color, af.CSPACE.GRAY, af.CSPACE.RGB) + img_color /= 255.0 + + ix, iy = af.gradient(img) + ixx = ix * ix + ixy = ix * iy + iyy = iy * iy + + # Compute a Gaussian kernel with standard deviation of 1.0 and length of 5 pixels + # These values can be changed to use a smaller or larger window + gauss_filt = af.gaussian_kernel(5, 5, 1.0, 1.0) + + # Filter second order derivatives + ixx = af.convolve(ixx, gauss_filt) + ixy = af.convolve(ixy, gauss_filt) + iyy = af.convolve(iyy, gauss_filt) + + # Calculate trace + itr = ixx + iyy + + # Calculate determinant + idet = ixx * iyy - ixy * ixy + + # Calculate Harris response + response = idet - 0.04 * (itr * itr) + + # Get maximum response for each 3x3 neighborhood + mask = af.constant(1, 3, 3) + max_resp = af.dilate(response, mask) + + # Discard responses that are not greater than threshold + corners = response > 1e5 + corners = corners * response + + # Discard responses that are not equal to maximum neighborhood response, + # scale them to original value + corners = (corners == max_resp) * corners + + # Copy device array to python list on host + corners_list = corners.to_list() + + draw_len = 3 + good_corners = 0 + for x in range(img_color.dims()[1]): + for y in range(img_color.dims()[0]): + if corners_list[x][y] > 1e5: + img_color = draw_corners(img_color, x, y, draw_len) + good_corners += 1 + + + print("Corners found: {}".format(good_corners)) + if not console: + # Previews color image with green crosshairs + wnd = af.Window(512, 512, "Harris Feature Detector") + + while not wnd.close(): + wnd.image(img_color) + else: + idx = af.where(corners) + + corners_x = idx / float(corners.dims()[0]) + corners_y = idx % float(corners.dims()[0]) + + print(corners_x) + print(corners_y) + + +if __name__ == "__main__": + if (len(sys.argv) > 1): + af.set_device(int(sys.argv[1])) + console = (sys.argv[2] == '-') if len(sys.argv) > 2 else False + + af.info() + print("** ArrayFire Harris Corner Detector Demo **\n") + + harris_demo(console) + diff --git a/examples/computer_vision/matching.py b/examples/computer_vision/matching.py new file mode 100644 index 000000000..cab0cccdf --- /dev/null +++ b/examples/computer_vision/matching.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2018, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from time import time +import arrayfire as af +import os +import sys + +def normalize(a): + max_ = float(af.max(a)) + min_ = float(af.min(a)) + return (a - min_) / (max_ - min_) + +def draw_rectangle(img, x, y, wx, wy): + print("\nMatching patch origin = ({}, {})\n".format(x, y)) + + # top edge + img[y, x : x + wx, 0] = 0.0 + img[y, x : x + wx, 1] = 0.0 + img[y, x : x + wx, 2] = 1.0 + + # bottom edge + img[y + wy, x : x + wx, 0] = 0.0 + img[y + wy, x : x + wx, 1] = 0.0 + img[y + wy, x : x + wx, 2] = 1.0 + + # left edge + img[y : y + wy, x, 0] = 0.0 + img[y : y + wy, x, 1] = 0.0 + img[y : y + wy, x, 2] = 1.0 + + # left edge + img[y : y + wy, x + wx, 0] = 0.0 + img[y : y + wy, x + wx, 1] = 0.0 + img[y : y + wy, x + wx, 2] = 1.0 + + return img + +def templateMatchingDemo(console): + + root_path = os.path.dirname(os.path.abspath(__file__)) + file_path = root_path + if console: + file_path += "/../../assets/examples/images/square.png" + else: + file_path += "/../../assets/examples/images/man.jpg" + img_color = af.load_image(file_path, True); + + # Convert the image from RGB to gray-scale + img = af.color_space(img_color, af.CSPACE.GRAY, af.CSPACE.RGB) + iDims = img.dims() + print("Input image dimensions: ", iDims) + + # Extract a patch from the input image + patch_size = 100 + tmp_img = img[100 : 100+patch_size, 100 : 100+patch_size] + + result = af.match_template(img, tmp_img) # Default disparity metric is + # Sum of Absolute differences (SAD) + # Currently supported metrics are + # AF_SAD, AF_ZSAD, AF_LSAD, AF_SSD, + # AF_ZSSD, AF_LSSD + + disp_img = img / 255.0 + disp_tmp = tmp_img / 255.0 + disp_res = normalize(result) + + minval, minloc = af.imin(disp_res) + print("Location(linear index) of minimum disparity value = {}".format(minloc)) + + if not console: + marked_res = af.tile(disp_img, 1, 1, 3) + marked_res = draw_rectangle(marked_res, minloc%iDims[0], minloc/iDims[0],\ + patch_size, patch_size) + + print("Note: Based on the disparity metric option provided to matchTemplate function") + print("either minimum or maximum disparity location is the starting corner") + print("of our best matching patch to template image in the search image") + + wnd = af.Window(512, 512, "Template Matching Demo") + + while not wnd.close(): + wnd.set_colormap(af.COLORMAP.DEFAULT) + wnd.grid(2, 2) + wnd[0, 0].image(disp_img, "Search Image" ) + wnd[0, 1].image(disp_tmp, "Template Patch" ) + wnd[1, 0].image(marked_res, "Best Match" ) + wnd.set_colormap(af.COLORMAP.HEAT) + wnd[1, 1].image(disp_res, "Disparity Values") + wnd.show() + + +if __name__ == "__main__": + if (len(sys.argv) > 1): + af.set_device(int(sys.argv[1])) + console = (sys.argv[2] == '-') if len(sys.argv) > 2 else False + + af.info() + print("** ArrayFire template matching Demo **\n") + templateMatchingDemo(console) + diff --git a/examples/computer_vision/susan.py b/examples/computer_vision/susan.py new file mode 100644 index 000000000..37a5dbd11 --- /dev/null +++ b/examples/computer_vision/susan.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2018, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from time import time +import arrayfire as af +import os +import sys + +def draw_corners(img, x, y, draw_len): + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + xmin = max(0, x - draw_len) + xmax = min(img.dims()[1], x + draw_len) + + img[y, xmin : xmax, 0] = 0.0 + img[y, xmin : xmax, 1] = 1.0 + img[y, xmin : xmax, 2] = 0.0 + + # Draw vertical line of (draw_len * 2 + 1) pixels centered on the corner + # Set only the first channel to 1 (green lines) + ymin = max(0, y - draw_len) + ymax = min(img.dims()[0], y + draw_len) + + img[ymin : ymax, x, 0] = 0.0 + img[ymin : ymax, x, 1] = 1.0 + img[ymin : ymax, x, 2] = 0.0 + return img + +def susan_demo(console): + + root_path = os.path.dirname(os.path.abspath(__file__)) + file_path = root_path + if console: + file_path += "/../../assets/examples/images/square.png" + else: + file_path += "/../../assets/examples/images/man.jpg" + img_color = af.load_image(file_path, True); + + img = af.color_space(img_color, af.CSPACE.GRAY, af.CSPACE.RGB) + img_color /= 255.0 + + features = af.susan(img) + + xs = features.get_xpos().to_list() + ys = features.get_ypos().to_list() + + draw_len = 3; + num_features = features.num_features().value + for f in range(num_features): + print(f) + x = xs[f] + y = ys[f] + + # TODO fix coord order to x,y after upstream fix + img_color = draw_corners(img_color, y, x, draw_len) + + + print("Features found: {}".format(num_features)) + if not console: + # Previews color image with green crosshairs + wnd = af.Window(512, 512, "SUSAN Feature Detector") + + while not wnd.close(): + wnd.image(img_color) + else: + print(xs); + print(ys); + + +if __name__ == "__main__": + if (len(sys.argv) > 1): + af.set_device(int(sys.argv[1])) + console = (sys.argv[2] == '-') if len(sys.argv) > 2 else False + + af.info() + print("** ArrayFire SUSAN Feature Detector Demo **\n") + susan_demo(console) + diff --git a/examples/lin_algebra/cholesky.py b/examples/lin_algebra/cholesky.py new file mode 100644 index 000000000..b3e550139 --- /dev/null +++ b/examples/lin_algebra/cholesky.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +####################################################### +# Copyright (c) 2022, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +import arrayfire as af + +def main(): + try: + af.info() + + n = 5 + t = af.randu(n, n) + arr_in = af.matmulNT(t, t) + af.identity(n, n) * n + + print("Running Cholesky InPlace\n") + cin_upper = arr_in.copy() + cin_lower = arr_in.copy() + + af.cholesky_inplace(cin_upper, True) + af.cholesky_inplace(cin_lower, False) + + print(cin_upper) + print(cin_lower) + + print("\nRunning Cholesky Out of place\n") + + out_upper, upper_success = af.cholesky(arr_in, True) + out_lower, lower_success = af.cholesky(arr_in, False) + + if upper_success == 0: + print(out_upper) + if lower_success == 0: + print(out_lower) + + except Exception as e: + print('Error: ', str(e)) + +if __name__ == '__main__': + main() diff --git a/examples/lin_algebra/lu.py b/examples/lin_algebra/lu.py new file mode 100644 index 000000000..14405cc29 --- /dev/null +++ b/examples/lin_algebra/lu.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +####################################################### +# Copyright (c) 2022, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +import arrayfire as af + +def main(): + try: + af.info() + + in_array = af.randu(5,8) + + print("Running LU InPlace\n") + pivot = af.lu_inplace(in_array) + print(in_array) + print(pivot) + + print("Running LU with Upper Lower Factorization\n") + lower, upper, pivot = af.lu(in_array) + print(lower) + print(upper) + print(pivot) + + except Exception as e: + print('Error: ', str(e)) + +if __name__ == '__main__': + main() + diff --git a/examples/lin_algebra/qr.py b/examples/lin_algebra/qr.py new file mode 100644 index 000000000..adeebfd0e --- /dev/null +++ b/examples/lin_algebra/qr.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +####################################################### +# Copyright (c) 2022, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +import arrayfire as af + +def main(): + try: + af.info() + in_array = af.randu(5,8) + + print("Running QR InPlace\n") + q_in = in_array.copy() + print(q_in) + + tau = af.qr_inplace(q_in) + + print(q_in) + print(tau) + + print("Running QR with Q and R factorization\n") + q, r, tau = af.qr(in_array) + + print(q) + print(r) + print(tau) + + except Exception as e: + print("Error: ", str(e)) + +if __name__ == '__main__': + main() diff --git a/examples/machine_learning/logistic_regression.py b/examples/machine_learning/logistic_regression.py new file mode 100644 index 000000000..b1d8b6e42 --- /dev/null +++ b/examples/machine_learning/logistic_regression.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2019, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from mnist_common import display_results, setup_mnist + +import sys +import time + +import arrayfire as af +from arrayfire.algorithm import max, imax, count, sum +from arrayfire.arith import abs, sigmoid, log +from arrayfire.array import transpose +from arrayfire.blas import matmul, matmulTN +from arrayfire.data import constant, join, lookup, moddims +from arrayfire.device import set_device, sync, eval + + +def accuracy(predicted, target): + _, tlabels = af.imax(target, 1) + _, plabels = af.imax(predicted, 1) + return 100 * af.count(plabels == tlabels) / tlabels.elements() + + +def abserr(predicted, target): + return 100 * af.sum(af.abs(predicted - target)) / predicted.elements() + + +# Predict (probability) based on given parameters +def predict_prob(X, Weights): + Z = af.matmul(X, Weights) + return af.sigmoid(Z) + + +# Predict (log probability) based on given parameters +def predict_log_prob(X, Weights): + return af.log(predict_prob(X, Weights)) + + +# Give most likely class based on given parameters +def predict_class(X, Weights): + probs = predict_prob(X, Weights) + _, classes = af.imax(probs, 1) + return classes + + +def cost(Weights, X, Y, lambda_param=1.0): + # Number of samples + m = Y.dims()[0] + + dim0 = Weights.dims()[0] + dim1 = Weights.dims()[1] if len(Weights.dims()) > 1 else None + dim2 = Weights.dims()[2] if len(Weights.dims()) > 2 else None + dim3 = Weights.dims()[3] if len(Weights.dims()) > 3 else None + # Make the lambda corresponding to Weights(0) == 0 + lambdat = af.constant(lambda_param, dim0, dim1, dim2, dim3) + + # No regularization for bias weights + lambdat[0, :] = 0 + + # Get the prediction + H = predict_prob(X, Weights) + + # Cost of misprediction + Jerr = -1 * af.sum(Y * af.log(H) + (1 - Y) * af.log(1 - H), dim=0) + + # Regularization cost + Jreg = 0.5 * af.sum(lambdat * Weights * Weights, dim=0) + + # Total cost + J = (Jerr + Jreg) / m + + # Find the gradient of cost + D = (H - Y) + dJ = (af.matmulTN(X, D) + lambdat * Weights) / m + + return J, dJ + + +def train(X, Y, alpha=0.1, lambda_param=1.0, maxerr=0.01, maxiter=1000, verbose=False): + # Initialize parameters to 0 + Weights = af.constant(0, X.dims()[1], Y.dims()[1]) + + for i in range(maxiter): + # Get the cost and gradient + J, dJ = cost(Weights, X, Y, lambda_param) + + err = af.max(af.abs(J)) + if err < maxerr: + print('Iteration {0:4d} Err: {1:4f}'.format(i + 1, err)) + print('Training converged') + return Weights + + if verbose and ((i+1) % 10 == 0): + print('Iteration {0:4d} Err: {1:4f}'.format(i + 1, err)) + + # Update the parameters via gradient descent + Weights = Weights - alpha * dJ + + if verbose: + print('Training stopped after {0:d} iterations'.format(maxiter)) + + return Weights + + +def benchmark_logistic_regression(train_feats, train_targets, test_feats): + t0 = time.time() + Weights = train(train_feats, train_targets, 0.1, 1.0, 0.01, 1000) + af.eval(Weights) + sync() + t1 = time.time() + dt = t1 - t0 + print('Training time: {0:4.4f} s'.format(dt)) + + t0 = time.time() + iters = 100 + for i in range(iters): + test_outputs = predict_prob(test_feats, Weights) + af.eval(test_outputs) + sync() + t1 = time.time() + dt = t1 - t0 + print('Prediction time: {0:4.4f} s'.format(dt / iters)) + + +# Demo of one vs all logistic regression +def logit_demo(console, perc): + # Load mnist data + frac = float(perc) / 100.0 + mnist_data = setup_mnist(frac, True) + num_classes = mnist_data[0] + num_train = mnist_data[1] + num_test = mnist_data[2] + train_images = mnist_data[3] + test_images = mnist_data[4] + train_targets = mnist_data[5] + test_targets = mnist_data[6] + + # Reshape images into feature vectors + feature_length = int(train_images.elements() / num_train); + train_feats = af.transpose(af.moddims(train_images, feature_length, num_train)) + test_feats = af.transpose(af.moddims(test_images, feature_length, num_test)) + + train_targets = af.transpose(train_targets) + test_targets = af.transpose(test_targets) + + num_train = train_feats.dims()[0] + num_test = test_feats.dims()[0] + + # Add a bias that is always 1 + train_bias = af.constant(1, num_train, 1) + test_bias = af.constant(1, num_test, 1) + train_feats = af.join(1, train_bias, train_feats) + test_feats = af.join(1, test_bias, test_feats) + + # Train logistic regression parameters + Weights = train(train_feats, train_targets, + 0.1, # learning rate + 1.0, # regularization constant + 0.01, # max error + 1000, # max iters + True # verbose mode + ) + af.eval(Weights) + af.sync() + + # Predict the results + train_outputs = predict_prob(train_feats, Weights) + test_outputs = predict_prob(test_feats, Weights) + + print('Accuracy on training data: {0:2.2f}'.format(accuracy(train_outputs, train_targets))) + print('Accuracy on testing data: {0:2.2f}'.format(accuracy(test_outputs, test_targets))) + print('Maximum error on testing data: {0:2.2f}'.format(abserr(test_outputs, test_targets))) + + benchmark_logistic_regression(train_feats, train_targets, test_feats) + + if not console: + test_outputs = af.transpose(test_outputs) + # Get 20 random test images + display_results(test_images, test_outputs, af.transpose(test_targets), 20, True) + +def main(): + argc = len(sys.argv) + + device = int(sys.argv[1]) if argc > 1 else 0 + console = sys.argv[2][0] == '-' if argc > 2 else False + perc = int(sys.argv[3]) if argc > 3 else 60 + + try: + af.set_device(device) + af.info() + logit_demo(console, perc) + except Exception as e: + print('Error: ', str(e)) + + +if __name__ == '__main__': + main() diff --git a/examples/machine_learning/mnist_common.py b/examples/machine_learning/mnist_common.py new file mode 100644 index 000000000..3f38302df --- /dev/null +++ b/examples/machine_learning/mnist_common.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2019, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +import os +import sys +sys.path.insert(0, '../common') +from idxio import read_idx + +import arrayfire as af +from arrayfire.algorithm import where +from arrayfire.array import Array +from arrayfire.data import constant, lookup, moddims +from arrayfire.random import randu + + +def classify(arr, k, expand_labels): + ret_str = '' + if expand_labels: + vec = arr[:, k].as_type(af.Dtype.f32) + h_vec = vec.to_list() + data = [] + + for i in range(vec.elements()): + data.append((h_vec[i], i)) + + data = sorted(data, key=lambda pair: pair[0], reverse=True) + + ret_str = str(data[0][1]) + + else: + ret_str = str(int(arr[k].as_type(af.Dtype.f32).scalar())) + + return ret_str + + +def setup_mnist(frac, expand_labels): + root_path = os.path.dirname(os.path.abspath(__file__)) + file_path = root_path + '/../../assets/examples/data/mnist/' + idims, idata = read_idx(file_path + 'images-subset') + ldims, ldata = read_idx(file_path + 'labels-subset') + + idims.reverse() + numdims = len(idims) + images = af.Array(idata, tuple(idims)) + + R = af.randu(10000, 1); + cond = R < min(frac, 0.8) + train_indices = af.where(cond) + test_indices = af.where(~cond) + + train_images = af.lookup(images, train_indices, 2) / 255 + test_images = af.lookup(images, test_indices, 2) / 255 + + num_classes = 10 + num_train = train_images.dims()[2] + num_test = test_images.dims()[2] + + if expand_labels: + train_labels = af.constant(0, num_classes, num_train) + test_labels = af.constant(0, num_classes, num_test) + + h_train_idx = train_indices.to_list() + h_test_idx = test_indices.to_list() + + for i in range(num_train): + train_labels[ldata[h_train_idx[i]], i] = 1 + + for i in range(num_test): + test_labels[ldata[h_test_idx[i]], i] = 1 + + else: + labels = af.Array(ldata, tuple(ldims)) + train_labels = labels[train_indices] + test_labels = labels[test_indices] + + return (num_classes, + num_train, + num_test, + train_images, + test_images, + train_labels, + test_labels) + + +def display_results(test_images, test_output, test_actual, num_display, expand_labels): + for i in range(num_display): + print('Predicted: ', classify(test_output, i, expand_labels)) + print('Actual: ', classify(test_actual, i, expand_labels)) + + img = (test_images[:, :, i] > 0.1).as_type(af.Dtype.u8) + img = af.moddims(img, img.elements()).to_list() + for j in range(28): + for k in range(28): + print('\u2588' if img[j * 28 + k] > 0 else ' ', end='') + print() + input() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..75335283e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel", "scikit-build", "cmake", "ninja"] 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 new file mode 100644 index 000000000..b40ac55e5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,63 @@ +[metadata] +name = arrayfire +version = 3.8.0 +description = Python bindings for ArrayFire +licence = BSD +long_description = file: README.md +long_description_content_type = text/markdown +maintainer = ArrayFire +maintainer_email = technical@arrayfire.com +url = http://arrayfire.com +classifiers = + Programming Language :: Python + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + +[options] +packages = find: +install_requires = + scikit-build +python_requires = + >=3.8.0 + +[options.packages.find] +include = arrayfire +exclude = + examples + tests +install_requires = + numpy~=1.22.0 + +[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 diff --git a/setup.py b/setup.py index 92900cc90..b6d37bb86 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python ####################################################### # Copyright (c) 2015, ArrayFire @@ -9,19 +9,98 @@ # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## -from setuptools import setup, find_packages -#from __af_version__ import full_version - -#TODO: -#1) Look for af libraries during setup - -setup( - author="Pavan Yalamanchili", - author_email="contact@pavanky.com", - name="arrayfire", - version="3.6.20181017", - description="Python bindings for ArrayFire", - license="BSD", - url="http://arrayfire.com", - packages=find_packages() -) +import os +import re + +# package can be distributed with arrayfire binaries or +# just with python wrapper files, the AF_BUILD_LOCAL +# environment var determines whether to build the arrayfire +# binaries locally rather than searching in a system install + +AF_BUILD_LOCAL_LIBS = os.environ.get('AF_BUILD_LOCAL_LIBS') +print(f'AF_BUILD_LOCAL_LIBS={AF_BUILD_LOCAL_LIBS}') +if AF_BUILD_LOCAL_LIBS: + print('Proceeding to build ArrayFire libraries') +else: + print('Skipping binaries installation, only python files will be installed') + +AF_BUILD_CPU = os.environ.get('AF_BUILD_CPU') +AF_BUILD_CPU = 1 if AF_BUILD_CPU is None else int(AF_BUILD_CPU) +AF_BUILD_CPU_CMAKE_STR = '-DAF_BUILD_CPU:BOOL=ON' if (AF_BUILD_CPU == 1) else '-DAF_BUILD_CPU:BOOL=OFF' + +AF_BUILD_CUDA = os.environ.get('AF_BUILD_CUDA') +AF_BUILD_CUDA = 1 if AF_BUILD_CUDA is None else int(AF_BUILD_CUDA) +AF_BUILD_CUDA_CMAKE_STR = '-DAF_BUILD_CUDA:BOOL=ON' if (AF_BUILD_CUDA == 1) else '-DAF_BUILD_CUDA:BOOL=OFF' + +AF_BUILD_OPENCL = os.environ.get('AF_BUILD_OPENCL') +AF_BUILD_OPENCL = 1 if AF_BUILD_OPENCL is None else int(AF_BUILD_OPENCL) +AF_BUILD_OPENCL_CMAKE_STR = '-DAF_BUILD_OPENCL:BOOL=ON' if (AF_BUILD_OPENCL == 1) else '-DAF_BUILD_OPENCL:BOOL=OFF' + +AF_BUILD_UNIFIED = os.environ.get('AF_BUILD_UNIFIED') +AF_BUILD_UNIFIED = 1 if AF_BUILD_UNIFIED is None else int(AF_BUILD_UNIFIED) +AF_BUILD_UNIFIED_CMAKE_STR = '-DAF_BUILD_UNIFIED:BOOL=ON' if (AF_BUILD_UNIFIED == 1) else '-DAF_BUILD_UNIFIED:BOOL=OFF' + +if AF_BUILD_LOCAL_LIBS: + # invoke cmake and build arrayfire libraries to install locally in package + from skbuild import setup + + def filter_af_files(cmake_manifest): + cmake_manifest = list(filter(lambda name: not (name.endswith('.h') + or name.endswith('.cpp') + or name.endswith('.hpp') + or name.endswith('.cmake') + or name.endswith('jpg') + or name.endswith('png') + or name.endswith('libaf.so') #avoids duplicates due to symlinks + or re.match('.*libaf\.so\.3\..*', name) is not None + or name.endswith('libafcpu.so') + or re.match('.*libafcpu\.so\.3\..*', name) is not None + or name.endswith('libafcuda.so') + or re.match('.*libafcuda\.so\.3\..*', name) is not None + or name.endswith('libafopencl.so') + or re.match('.*libafopencl\.so\.3\..*', name) is not None + or name.endswith('libforge.so') + or re.match('.*libforge\.so\.1\..*', name) is not None + or 'examples' in name), cmake_manifest)) + return cmake_manifest + + print('Building CMAKE with following configurable variables: ') + print(AF_BUILD_CPU_CMAKE_STR) + print(AF_BUILD_CUDA_CMAKE_STR) + print(AF_BUILD_OPENCL_CMAKE_STR) + print(AF_BUILD_UNIFIED_CMAKE_STR) + + + setup( + packages=['arrayfire'], + cmake_install_dir='', + cmake_process_manifest_hook=filter_af_files, + include_package_data=False, + cmake_args=[AF_BUILD_CPU_CMAKE_STR, + AF_BUILD_CUDA_CMAKE_STR, + AF_BUILD_OPENCL_CMAKE_STR, + AF_BUILD_UNIFIED_CMAKE_STR, + # todo: pass additional args from environ + '-DCMAKE_BUILD_TYPE:STRING="RelWithDebInfo"', + '-DFG_USE_STATIC_CPPFLAGS:BOOL=OFF', + '-DFG_WITH_FREEIMAGE:BOOL=OFF', + '-DCUDA_architecture_build_targets:STRING=All', + '-DAF_BUILD_DOCS:BOOL=OFF', + '-DAF_BUILD_EXAMPLES:BOOL=OFF', + '-DAF_INSTALL_STANDALONE:BOOL=ON', + '-DAF_WITH_IMAGEIO:BOOL=ON', + '-DAF_WITH_LOGGING:BOOL=ON', + '-DBUILD_TESTING:BOOL=OFF', + '-DAF_BUILD_FORGE:BOOL=ON', + '-DAF_INSTALL_LIB_DIR:STRING=arrayfire', + '-DAF_INSTALL_BIN_DIR:STRING=arrayfire', + '-DFG_INSTALL_LIB_DIR:STRING=arrayfire', + '-DAF_WITH_STATIC_MKL=ON', + ] + ) + +else: + # ignores local arrayfire libraries, will search system instead + from setuptools import setup + setup() + diff --git a/arrayfire/tests/__init__.py b/tests/__init__.py similarity index 93% rename from arrayfire/tests/__init__.py rename to tests/__init__.py index be7669881..24e9ac759 100644 --- a/arrayfire/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. diff --git a/arrayfire/tests/__main__.py b/tests/__main__.py similarity index 75% rename from arrayfire/tests/__main__.py rename to tests/__main__.py index 468396566..6bed94278 100644 --- a/arrayfire/tests/__main__.py +++ b/tests/__main__.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -10,35 +12,38 @@ from __future__ import absolute_import import sys -from .simple_tests import * + +from . import simple tests = {} tests['simple'] = simple.tests + def assert_valid(name, name_list, name_str): is_valid = any([name == val for val in name_list]) - if not is_valid: - err_str = "The first argument needs to be a %s name\n" % name_str - err_str += "List of supported %ss: %s" % (name_str, str(list(name_list))) - raise RuntimeError(err_str) + if is_valid: + return + err_str = "The first argument needs to be a %s name\n" % name_str + err_str += "List of supported %ss: %s" % (name_str, str(list(name_list))) + raise RuntimeError(err_str) -if __name__ == "__main__": +if __name__ == "__main__": module_name = None num_args = len(sys.argv) - if (num_args > 1): + if num_args > 1: module_name = sys.argv[1].lower() assert_valid(sys.argv[1].lower(), tests.keys(), "module") - if (module_name is None): + if module_name is None: for name in tests: tests[name].run() else: test = tests[module_name] test_list = None - if (num_args > 2): + if num_args > 2: test_list = sys.argv[2:] for test_name in test_list: assert_valid(test_name.lower(), test.keys(), "test") diff --git a/tests/simple/__init__.py b/tests/simple/__init__.py new file mode 100644 index 000000000..4950136e3 --- /dev/null +++ b/tests/simple/__init__.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +####################################################### +# Copyright (c) 2015, ArrayFire +# All rights reserved. +# +# This file is distributed under 3-clause BSD license. +# The complete license agreement can be obtained at: +# http://arrayfire.com/licenses/BSD-3-Clause +######################################################## + +from ._util import tests +from .algorithm import simple_algorithm +from .arith import simple_arith +from .array_test import simple_array +from .blas import simple_blas +from .data import simple_data +from .device import simple_device +from .image import simple_image +from .index import simple_index +from .interop import simple_interop +from .lapack import simple_lapack +from .random import simple_random +from .signal import simple_signal +from .sparse import simple_sparse +from .statistics import simple_statistics + +__all__ = [ + "tests", + "simple_algorithm", + "simple_arith", + "simple_array", + "simple_blas", + "simple_data", + "simple_device", + "simple_image", + "simple_index", + "simple_interop", + "simple_lapack", + "simple_random", + "simple_signal", + "simple_sparse", + "simple_statistics" +] diff --git a/arrayfire/tests/simple/_util.py b/tests/simple/_util.py similarity index 88% rename from arrayfire/tests/simple/_util.py rename to tests/simple/_util.py index cda7c84a8..2275cf2fc 100644 --- a/arrayfire/tests/simple/_util.py +++ b/tests/simple/_util.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -6,13 +8,13 @@ # The complete license agreement can be obtained at: # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## -import traceback + import logging -import arrayfire as af import sys +import traceback -class _simple_test_dict(dict): +class _simple_test_dict(dict): def __init__(self): self.print_str = "Simple %16s: %s" self.failed = False @@ -21,7 +23,7 @@ def __init__(self): def run(self, name_list=None, verbose=False): test_list = name_list if name_list is not None else self.keys() for key in test_list: - self.print_log = '' + self.print_log = "" try: test = self[key] except KeyError: @@ -31,27 +33,30 @@ def run(self, name_list=None, verbose=False): try: test(verbose) print(self.print_str % (key, "PASSED")) - except Exception as e: + except Exception: print(self.print_str % (key, "FAILED")) self.failed = True - if (not verbose): + if not verbose: print(tests.print_log) logging.error(traceback.format_exc()) - if (self.failed): + if self.failed: sys.exit(1) + tests = _simple_test_dict() + def print_func(verbose): def print_func_impl(*args): - _print_log = '' + _print_log = "" for arg in args: _print_log += str(arg) + '\n' - if (verbose): + if verbose: print(_print_log) tests.print_log += _print_log return print_func_impl + def display_func(verbose): return print_func(verbose) diff --git a/arrayfire/tests/simple/algorithm.py b/tests/simple/algorithm.py similarity index 58% rename from arrayfire/tests/simple/algorithm.py rename to tests/simple/algorithm.py index 5528da3b0..b9e42f138 100644 --- a/arrayfire/tests/simple/algorithm.py +++ b/tests/simple/algorithm.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,22 +10,47 @@ ######################################################## import arrayfire as af + from . import _util -def simple_algorithm(verbose = False): + +def simple_algorithm(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) + print_func = _util.print_func(verbose) a = af.randu(3, 3) k = af.constant(1, 3, 3, dtype=af.Dtype.u32) af.eval(k) - print_func(af.sum(a), af.product(a), af.min(a), af.max(a), - af.count(a), af.any_true(a), af.all_true(a)) + print_func(af.sum(a), af.product(a), af.min(a), af.max(a), af.count(a), af.any_true(a), af.all_true(a)) display_func(af.sum(a, 0)) display_func(af.sum(a, 1)) + rk = af.constant(1, 3, dtype=af.Dtype.u32) + rk[2] = 0 + af.eval(rk) + display_func(af.sumByKey(rk, a, dim=0)) + display_func(af.sumByKey(rk, a, dim=1)) + + display_func(af.productByKey(rk, a, dim=0)) + display_func(af.productByKey(rk, a, dim=1)) + + display_func(af.minByKey(rk, a, dim=0)) + display_func(af.minByKey(rk, a, dim=1)) + + display_func(af.maxByKey(rk, a, dim=0)) + display_func(af.maxByKey(rk, a, dim=1)) + + display_func(af.anyTrueByKey(rk, a, dim=0)) + display_func(af.anyTrueByKey(rk, a, dim=1)) + + display_func(af.allTrueByKey(rk, a, dim=0)) + display_func(af.allTrueByKey(rk, a, dim=1)) + + display_func(af.countByKey(rk, a, dim=0)) + display_func(af.countByKey(rk, a, dim=1)) + display_func(af.product(a, 0)) display_func(af.product(a, 1)) @@ -58,27 +84,27 @@ def simple_algorithm(verbose = False): b = (a > 0.1) * a c = (a > 0.4) * a d = b / c - print_func(af.sum(d)); - print_func(af.sum(d, nan_val=0.0)); - display_func(af.sum(d, dim=0, nan_val=0.0)); + print_func(af.sum(d)) + print_func(af.sum(d, nan_val=0.0)) + display_func(af.sum(d, dim=0, nan_val=0.0)) - val,idx = af.sort_index(a, is_ascending=True) + val, idx = af.sort_index(a, is_ascending=True) display_func(val) display_func(idx) - val,idx = af.sort_index(a, is_ascending=False) + val, idx = af.sort_index(a, is_ascending=False) display_func(val) display_func(idx) - b = af.randu(3,3) - keys,vals = af.sort_by_key(a, b, is_ascending=True) + b = af.randu(3, 3) + keys, vals = af.sort_by_key(a, b, is_ascending=True) display_func(keys) display_func(vals) - keys,vals = af.sort_by_key(a, b, is_ascending=False) + keys, vals = af.sort_by_key(a, b, is_ascending=False) display_func(keys) display_func(vals) - c = af.randu(5,1) - d = af.randu(5,1) + c = af.randu(5, 1) + d = af.randu(5, 1) cc = af.set_unique(c, is_sorted=False) dd = af.set_unique(af.sort(d), is_sorted=True) display_func(cc) @@ -90,4 +116,5 @@ def simple_algorithm(verbose = False): display_func(af.set_intersect(cc, cc, is_unique=True)) display_func(af.set_intersect(cc, cc, is_unique=False)) -_util.tests['algorithm'] = simple_algorithm + +_util.tests["algorithm"] = simple_algorithm diff --git a/arrayfire/tests/simple/arith.py b/tests/simple/arith.py similarity index 89% rename from arrayfire/tests/simple/arith.py rename to tests/simple/arith.py index 84c291aec..5d4d83d00 100644 --- a/arrayfire/tests/simple/arith.py +++ b/tests/simple/arith.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,13 +10,14 @@ ######################################################## import arrayfire as af + from . import _util -def simple_arith(verbose = False): + +def simple_arith(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) - a = af.randu(3,3) + a = af.randu(3, 3) b = af.constant(4, 3, 3) display_func(a) display_func(b) @@ -29,7 +31,6 @@ def simple_arith(verbose = False): display_func(a + 2) display_func(3 + a) - c = a - b d = a d -= b @@ -99,7 +100,7 @@ def simple_arith(verbose = False): display_func(a == 0.5) display_func(0.5 == a) - a = af.randu(3,3,dtype=af.Dtype.u32) + a = af.randu(3, 3, dtype=af.Dtype.u32) b = af.constant(4, 3, 3, dtype=af.Dtype.u32) display_func(a & b) @@ -132,17 +133,17 @@ def simple_arith(verbose = False): display_func(a) display_func(af.cast(a, af.Dtype.c32)) - display_func(af.maxof(a,b)) - display_func(af.minof(a,b)) + display_func(af.maxof(a, b)) + display_func(af.minof(a, b)) display_func(af.clamp(a, 0, 1)) display_func(af.clamp(a, 0, b)) display_func(af.clamp(a, b, 1)) - display_func(af.rem(a,b)) + display_func(af.rem(a, b)) - a = af.randu(3,3) - 0.5 - b = af.randu(3,3) - 0.5 + a = af.randu(3, 3) - 0.5 + b = af.randu(3, 3) - 0.5 display_func(af.abs(a)) display_func(af.arg(a)) @@ -161,7 +162,7 @@ def simple_arith(verbose = False): display_func(af.atan2(a, b)) c = af.cplx(a) - d = af.cplx(a,b) + d = af.cplx(a, b) display_func(c) display_func(d) display_func(af.real(d)) @@ -191,10 +192,11 @@ def simple_arith(verbose = False): display_func(af.log10(a)) display_func(af.log2(a)) display_func(af.sqrt(a)) + display_func(af.rsqrt(a)) display_func(af.cbrt(a)) - a = af.round(5 * af.randu(3,3) - 1) - b = af.round(5 * af.randu(3,3) - 1) + a = af.round(5 * af.randu(3, 3) - 1) + b = af.round(5 * af.randu(3, 3) - 1) display_func(af.factorial(a)) display_func(af.tgamma(a)) @@ -205,7 +207,7 @@ def simple_arith(verbose = False): a = af.randu(5, 1) b = af.randu(1, 5) - c = af.broadcast(lambda x,y: x+y, a, b) + c = af.broadcast(lambda x, y: x+y, a, b) display_func(a) display_func(b) display_func(c) @@ -216,4 +218,5 @@ def test_add(aa, bb): display_func(test_add(a, b)) -_util.tests['arith'] = simple_arith + +_util.tests["arith"] = simple_arith diff --git a/arrayfire/tests/simple/array_test.py b/tests/simple/array_test.py similarity index 88% rename from arrayfire/tests/simple/array_test.py rename to tests/simple/array_test.py index 0c6ab5262..b2a787940 100644 --- a/arrayfire/tests/simple/array_test.py +++ b/tests/simple/array_test.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -8,13 +9,16 @@ # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## -import arrayfire as af import array as host + +import arrayfire as af + from . import _util + def simple_array(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) + print_func = _util.print_func(verbose) a = af.Array([1, 2, 3]) display_func(a) @@ -30,15 +34,14 @@ def simple_array(verbose=False): print_func(a.is_complex(), a.is_real(), a.is_double(), a.is_single()) print_func(a.is_real_floating(), a.is_floating(), a.is_integer(), a.is_bool()) - - a = af.Array(host.array('i', [4, 5, 6])) + a = af.Array(host.array("i", [4, 5, 6])) display_func(a) print_func(a.elements(), a.type(), a.dims(), a.numdims()) print_func(a.is_empty(), a.is_scalar(), a.is_column(), a.is_row()) print_func(a.is_complex(), a.is_real(), a.is_double(), a.is_single()) print_func(a.is_real_floating(), a.is_floating(), a.is_integer(), a.is_bool()) - a = af.Array(host.array('I', [7, 8, 9] * 3), (3,3)) + a = af.Array(host.array("I", [7, 8, 9] * 3), (3, 3)) display_func(a) print_func(a.elements(), a.type(), a.dims(), a.numdims()) print_func(a.is_empty(), a.is_scalar(), a.is_column(), a.is_row()) @@ -49,7 +52,7 @@ def simple_array(verbose=False): for n in range(a.elements()): print_func(c[n]) - c,s = a.to_ctype(True, True) + c, s = a.to_ctype(True, True) for n in range(a.elements()): print_func(c[n]) print_func(s) @@ -62,4 +65,5 @@ def simple_array(verbose=False): print_func(a.is_sparse()) -_util.tests['array'] = simple_array + +_util.tests["array"] = simple_array diff --git a/arrayfire/tests/simple/blas.py b/tests/simple/blas.py similarity index 56% rename from arrayfire/tests/simple/blas.py rename to tests/simple/blas.py index fd58d18d9..f04049a93 100644 --- a/arrayfire/tests/simple/blas.py +++ b/tests/simple/blas.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,19 +10,21 @@ ######################################################## import arrayfire as af + from . import _util + def simple_blas(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) - a = af.randu(5,5) - b = af.randu(5,5) + a = af.randu(5, 5) + b = af.randu(5, 5) + + display_func(af.matmul(a, b)) + display_func(af.matmul(a, b, af.MATPROP.TRANS)) + display_func(af.matmul(a, b, af.MATPROP.NONE, af.MATPROP.TRANS)) - display_func(af.matmul(a,b)) - display_func(af.matmul(a,b,af.MATPROP.TRANS)) - display_func(af.matmul(a,b,af.MATPROP.NONE, af.MATPROP.TRANS)) + b = af.randu(5, 1) + display_func(af.dot(b, b)) - b = af.randu(5,1) - display_func(af.dot(b,b)) -_util.tests['blas'] = simple_blas +_util.tests["blas"] = simple_blas diff --git a/arrayfire/tests/simple/data.py b/tests/simple/data.py similarity index 71% rename from arrayfire/tests/simple/data.py rename to tests/simple/data.py index 86b900baf..d091497eb 100644 --- a/arrayfire/tests/simple/data.py +++ b/tests/simple/data.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,20 +10,21 @@ ######################################################## import arrayfire as af + from . import _util + def simple_data(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) - display_func(af.constant(100, 3,3, dtype=af.Dtype.f32)) - display_func(af.constant(25, 3,3, dtype=af.Dtype.c32)) - display_func(af.constant(2**50, 3,3, dtype=af.Dtype.s64)) - display_func(af.constant(2+3j, 3,3)) - display_func(af.constant(3+5j, 3,3, dtype=af.Dtype.c32)) + display_func(af.constant(100, 3, 3, dtype=af.Dtype.f32)) + display_func(af.constant(25, 3, 3, dtype=af.Dtype.c32)) + display_func(af.constant(2**50, 3, 3, dtype=af.Dtype.s64)) + display_func(af.constant(2+3j, 3, 3)) + display_func(af.constant(3+5j, 3, 3, dtype=af.Dtype.c32)) display_func(af.range(3, 3)) - display_func(af.iota(3, 3, tile_dims=(2,2))) + display_func(af.iota(3, 3, tile_dims=(2, 2))) display_func(af.identity(3, 3, 1, 2, af.Dtype.b8)) display_func(af.identity(3, 3, dtype=af.Dtype.c32)) @@ -35,15 +37,14 @@ def simple_data(verbose=False): display_func(b) display_func(c) - display_func(af.diag(b, extract = False)) - display_func(af.diag(c, 1, extract = False)) + display_func(af.diag(b, extract=False)) + display_func(af.diag(c, 1, extract=False)) display_func(af.join(0, a, a)) display_func(af.join(1, a, a, a)) display_func(af.tile(a, 2, 2)) - display_func(af.reorder(a, 1, 0)) display_func(af.shift(a, -1, 1)) @@ -61,7 +62,7 @@ def simple_data(verbose=False): display_func(af.upper(a, False)) display_func(af.upper(a, True)) - a = af.randu(5,5) + a = af.randu(5, 5) display_func(af.transpose(a)) af.transpose_inplace(a) display_func(a) @@ -71,4 +72,6 @@ def simple_data(verbose=False): af.replace(a, a > 0.3, -0.3) display_func(a) -_util.tests['data'] = simple_data + display_func(af.pad(a, (1, 1, 0, 0), (2, 2, 0, 0))) + +_util.tests["data"] = simple_data diff --git a/arrayfire/tests/simple/device.py b/tests/simple/device.py similarity index 79% rename from arrayfire/tests/simple/device.py rename to tests/simple/device.py index 279fa3168..f677c5e2a 100644 --- a/arrayfire/tests/simple/device.py +++ b/tests/simple/device.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,11 +10,13 @@ ######################################################## import arrayfire as af + from . import _util + def simple_device(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) + print_func = _util.print_func(verbose) print_func(af.device_info()) print_func(af.get_device_count()) print_func(af.is_dbl_supported()) @@ -35,19 +38,19 @@ def simple_device(verbose=False): a = af.randu(100, 100) af.sync(dev) mem_info = af.device_mem_info() - assert(mem_info['alloc']['buffers'] == 1 + mem_info_old['alloc']['buffers']) - assert(mem_info[ 'lock']['buffers'] == 1 + mem_info_old[ 'lock']['buffers']) + assert(mem_info["alloc"]["buffers"] == 1 + mem_info_old["alloc"]["buffers"]) + assert(mem_info["lock"]["buffers"] == 1 + mem_info_old["lock"]["buffers"]) af.set_device(curr_dev) - a = af.randu(10,10) + a = af.randu(10, 10) display_func(a) dev_ptr = af.get_device_ptr(a) print_func(dev_ptr) b = af.Array(src=dev_ptr, dims=a.dims(), dtype=a.dtype(), is_device=True) display_func(b) - c = af.randu(10,10) + c = af.randu(10, 10) af.lock_array(c) af.unlock_array(c) @@ -64,10 +67,11 @@ def simple_device(verbose=False): print_func(d) print_func(af.set_manual_eval_flag(True)) - assert(af.get_manual_eval_flag() == True) + assert(af.get_manual_eval_flag()) print_func(af.set_manual_eval_flag(False)) - assert(af.get_manual_eval_flag() == False) + assert(not af.get_manual_eval_flag()) display_func(af.is_locked_array(a)) -_util.tests['device'] = simple_device + +_util.tests["device"] = simple_device diff --git a/arrayfire/tests/simple/image.py b/tests/simple/image.py similarity index 70% rename from arrayfire/tests/simple/image.py rename to tests/simple/image.py index 9c7887ebb..6f2e12186 100644 --- a/arrayfire/tests/simple/image.py +++ b/tests/simple/image.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,23 +10,24 @@ ######################################################## import arrayfire as af + from . import _util -def simple_image(verbose = False): + +def simple_image(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) a = 10 * af.randu(6, 6) - a3 = 10 * af.randu(5,5,3) + a3 = 10 * af.randu(5, 5, 3) - dx,dy = af.gradient(a) + dx, dy = af.gradient(a) display_func(dx) display_func(dy) display_func(af.resize(a, scale=0.5)) display_func(af.resize(a, odim0=8, odim1=8)) - t = af.randu(3,2) + t = af.randu(3, 2) display_func(af.transform(a, t)) display_func(af.rotate(a, 3.14)) display_func(af.translate(a, 1, 1)) @@ -49,8 +51,11 @@ def simple_image(verbose = False): display_func(af.maxfilt(a)) display_func(af.regions(af.round(a) > 3)) + display_func(af.confidenceCC(af.randu(10, 10), + (af.randu(2) * 9).as_type(af.Dtype.u32), (af.randu(2) * 9).as_type(af.Dtype.u32), 3, 3, 10, 0.1)) + - dx,dy = af.sobel_derivatives(a) + dx, dy = af.sobel_derivatives(a) display_func(dx) display_func(dy) display_func(af.sobel_filter(a)) @@ -66,7 +71,7 @@ def simple_image(verbose = False): display_func(af.color_space(a, af.CSPACE.RGB, af.CSPACE.GRAY)) - a = af.randu(6,6) + a = af.randu(6, 6) b = af.unwrap(a, 2, 2, 2, 2) c = af.wrap(b, 6, 6, 2, 2, 2, 2) display_func(a) @@ -74,13 +79,21 @@ def simple_image(verbose = False): display_func(c) display_func(af.sat(a)) - a = af.randu(10,10,3) + a = af.randu(10, 10, 3) display_func(af.rgb2ycbcr(a)) display_func(af.ycbcr2rgb(a)) a = af.randu(10, 10) - b = af.canny(a, low_threshold = 0.2, high_threshold = 0.8) + b = af.canny(a, low_threshold=0.2, high_threshold=0.8) display_func(af.anisotropic_diffusion(a, 0.125, 1.0, 64, af.FLUX.QUADRATIC, af.DIFFUSION.GRAD)) -_util.tests['image'] = simple_image + a = af.randu(10, 10) + psf = af.gaussian_kernel(3, 3) + cimg = af.convolve(a, psf) + display_func(af.iterativeDeconv(cimg, psf, 100, 0.5, af.ITERATIVE_DECONV.LANDWEBER)) + display_func(af.iterativeDeconv(cimg, psf, 100, 0.5, af.ITERATIVE_DECONV.RICHARDSONLUCY)) + display_func(af.inverseDeconv(cimg, psf, 1.0, af.INVERSE_DECONV.TIKHONOV)) + + +_util.tests["image"] = simple_image diff --git a/arrayfire/tests/simple/index.py b/tests/simple/index.py similarity index 68% rename from arrayfire/tests/simple/index.py rename to tests/simple/index.py index 7ebde6eb3..8bb4b571a 100644 --- a/arrayfire/tests/simple/index.py +++ b/tests/simple/index.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -7,14 +8,17 @@ # The complete license agreement can be obtained at: # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## + +import array as host + import arrayfire as af from arrayfire import ParallelRange -import array as host + from . import _util + def simple_index(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) a = af.randu(5, 5) display_func(a) b = af.Array(a) @@ -22,16 +26,16 @@ def simple_index(verbose=False): c = a.copy() display_func(c) - display_func(a[0,0]) + display_func(a[0, 0]) display_func(a[0]) display_func(a[:]) - display_func(a[:,:]) - display_func(a[0:3,]) - display_func(a[-2:-1,-1]) + display_func(a[:, :]) + display_func(a[0:3, ]) + display_func(a[-2:-1, -1]) display_func(a[0:5]) display_func(a[0:5:2]) - idx = af.Array(host.array('i', [0, 3, 2])) + idx = af.Array(host.array("i", [0, 3, 2])) display_func(idx) aa = a[idx] display_func(aa) @@ -40,42 +44,42 @@ def simple_index(verbose=False): display_func(a) a[0] = af.randu(1, 5) display_func(a) - a[:] = af.randu(5,5) + a[:] = af.randu(5, 5) display_func(a) - a[:,-1] = af.randu(5,1) + a[:, -1] = af.randu(5, 1) display_func(a) a[0:5:2] = af.randu(3, 5) display_func(a) - a[idx, idx] = af.randu(3,3) + a[idx, idx] = af.randu(3, 3) display_func(a) - - a = af.randu(5,1) - b = af.randu(5,1) + a = af.randu(5, 1) + b = af.randu(5, 1) display_func(a) display_func(b) - for ii in ParallelRange(1,3): + for ii in ParallelRange(1, 3): a[ii] = b[ii] display_func(a) - for ii in ParallelRange(2,5): + for ii in ParallelRange(2, 5): b[ii] = 2 display_func(b) - a = af.randu(3,2) + a = af.randu(3, 2) rows = af.constant(0, 1, dtype=af.Dtype.s32) - b = a[:,rows] + b = a[:, rows] display_func(b) - for r in rows: + for r in range(rows.elements()): display_func(r) - display_func(b[:,r]) + display_func(b[:, r]) a = af.randu(3) c = af.randu(3) - b = af.constant(1,3,dtype=af.Dtype.b8) + b = af.constant(1, 3, dtype=af.Dtype.b8) display_func(a) a[b] = c display_func(a) -_util.tests['index'] = simple_index + +_util.tests["index"] = simple_index diff --git a/arrayfire/tests/simple/interop.py b/tests/simple/interop.py similarity index 68% rename from arrayfire/tests/simple/interop.py rename to tests/simple/interop.py index ee924c6fa..f07e6a770 100644 --- a/arrayfire/tests/simple/interop.py +++ b/tests/simple/interop.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,72 +10,73 @@ ######################################################## import arrayfire as af + from . import _util -def simple_interop(verbose = False): + +def simple_interop(verbose=False): if af.AF_NUMPY_FOUND: import numpy as np n = np.random.random((5,)) a = af.to_array(n) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) n2[:] = 0 a.to_ndarray(n2) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3)) + n = np.random.random((5, 3)) a = af.to_array(n) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) n2[:] = 0 a.to_ndarray(n2) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2)) + n = np.random.random((5, 3, 2)) a = af.to_array(n) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) n2[:] = 0 a.to_ndarray(n2) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2,2)) + n = np.random.random((5, 3, 2, 2)) a = af.to_array(n) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) n2[:] = 0 a.to_ndarray(n2) - assert((n==n2).all()) + assert((n == n2).all()) - if af.AF_PYCUDA_FOUND and af.get_active_backend() == 'cuda': - import pycuda.autoinit + if af.AF_PYCUDA_FOUND and af.get_active_backend() == "cuda": import pycuda.gpuarray as cudaArray n = np.random.random((5,)) c = cudaArray.to_gpu(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3)) + n = np.random.random((5, 3)) c = cudaArray.to_gpu(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2)) + n = np.random.random((5, 3, 2)) c = cudaArray.to_gpu(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2,2)) + n = np.random.random((5, 3, 2, 2)) c = cudaArray.to_gpu(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - if af.AF_PYOPENCL_FOUND and af.backend.name() == 'opencl': - # This needs fixing upstream + if af.AF_PYOPENCL_FOUND and af.backend.name() == "opencl": + # TODO: This needs fixing upstream # https://github.com/arrayfire/arrayfire/issues/1728 # import pyopencl as cl @@ -107,33 +109,32 @@ def simple_interop(verbose = False): # assert((n==n2).all()) pass - if af.AF_NUMBA_FOUND and af.get_active_backend() == 'cuda': - - import numba + if af.AF_NUMBA_FOUND and af.get_active_backend() == "cuda": from numba import cuda n = np.random.random((5,)) c = cuda.to_device(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3)) + n = np.random.random((5, 3)) c = cuda.to_device(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2)) + n = np.random.random((5, 3, 2)) c = cuda.to_device(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) - n = np.random.random((5,3,2,2)) + n = np.random.random((5, 3, 2, 2)) c = cuda.to_device(n) a = af.to_array(c) n2 = np.array(a) - assert((n==n2).all()) + assert((n == n2).all()) + -_util.tests['interop'] = simple_interop +_util.tests["interop"] = simple_interop diff --git a/arrayfire/tests/simple/lapack.py b/tests/simple/lapack.py similarity index 76% rename from arrayfire/tests/simple/lapack.py rename to tests/simple/lapack.py index e3c3dbbb2..8cd3e9ac3 100644 --- a/arrayfire/tests/simple/lapack.py +++ b/tests/simple/lapack.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -7,15 +8,18 @@ # The complete license agreement can be obtained at: # http://arrayfire.com/licenses/BSD-3-Clause ######################################################## + import arrayfire as af + from . import _util + def simple_lapack(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) - a = af.randu(5,5) + print_func = _util.print_func(verbose) + a = af.randu(5, 5) - l,u,p = af.lu(a) + l, u, p = af.lu(a) display_func(l) display_func(u) @@ -26,9 +30,9 @@ def simple_lapack(verbose=False): display_func(a) display_func(p) - a = af.randu(5,3) + a = af.randu(5, 3) - q,r,t = af.qr(a) + q, r, t = af.qr(a) display_func(q) display_func(r) @@ -39,21 +43,24 @@ def simple_lapack(verbose=False): display_func(a) a = af.randu(5, 5) - a = af.matmulTN(a, a.copy()) + 10 * af.identity(5,5) + a = af.matmulTN(a, a.copy()) + 10 * af.identity(5, 5) - R,info = af.cholesky(a) + R, info = af.cholesky(a) display_func(R) print_func(info) af.cholesky_inplace(a) display_func(a) - a = af.randu(5,5) + a = af.randu(5, 5) ai = af.inverse(a) display_func(a) display_func(ai) + ai = af.pinverse(a) + display_func(ai) + x0 = af.randu(5, 3) b = af.matmul(a, x0) x1 = af.solve(a, b) @@ -74,11 +81,12 @@ def simple_lapack(verbose=False): print_func(af.norm(a, af.NORM.MATRIX_INF)) print_func(af.norm(a, af.NORM.MATRIX_L_PQ, 1, 1)) - a = af.randu(10,10) + a = af.randu(10, 10) display_func(a) - u,s,vt = af.svd(a) + u, s, vt = af.svd(a) display_func(af.matmul(af.matmul(u, af.diag(s, 0, False)), vt)) - u,s,vt = af.svd_inplace(a) + u, s, vt = af.svd_inplace(a) display_func(af.matmul(af.matmul(u, af.diag(s, 0, False)), vt)) -_util.tests['lapack'] = simple_lapack + +_util.tests["lapack"] = simple_lapack diff --git a/arrayfire/tests/simple/random.py b/tests/simple/random.py similarity index 91% rename from arrayfire/tests/simple/random.py rename to tests/simple/random.py index 544389836..5152cb4e0 100644 --- a/arrayfire/tests/simple/random.py +++ b/tests/simple/random.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,11 +10,12 @@ ######################################################## import arrayfire as af + from . import _util + def simple_random(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) display_func(af.randu(3, 3, 1, 2)) display_func(af.randu(3, 3, 1, 2, af.Dtype.b8)) @@ -35,4 +37,5 @@ def simple_random(verbose=False): engine.set_seed(100) assert(engine.get_seed() == 100) -_util.tests['random'] = simple_random + +_util.tests["random"] = simple_random diff --git a/arrayfire/tests/simple/signal.py b/tests/simple/signal.py similarity index 84% rename from arrayfire/tests/simple/signal.py rename to tests/simple/signal.py index fa8036dc2..0e3e8da9d 100644 --- a/arrayfire/tests/simple/signal.py +++ b/tests/simple/signal.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,24 +10,25 @@ ######################################################## import arrayfire as af + from . import _util + def simple_signal(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) signal = af.randu(10) - x_new = af.randu(10) + x_new = af.randu(10) x_orig = af.randu(10) - display_func(af.approx1(signal, x_new, xp = x_orig)) + display_func(af.approx1(signal, x_new, xp=x_orig)) signal = af.randu(3, 3) - x_new = af.randu(3, 3) + x_new = af.randu(3, 3) x_orig = af.randu(3, 3) - y_new = af.randu(3, 3) + y_new = af.randu(3, 3) y_orig = af.randu(3, 3) - display_func(af.approx2(signal, x_new, y_new, xp = x_orig, yp = y_orig)) + display_func(af.approx2(signal, x_new, y_new, xp=x_orig, yp=y_orig)) a = af.randu(8, 1) display_func(a) @@ -99,6 +101,13 @@ def simple_signal(verbose=False): display_func(af.convolve(a, b)) display_func(af.fft_convolve(a, b)) + c = af.convolve2NN(a, b) + display_func(c) + in_dims = c.dims() + incoming_grad = af.constant(1, in_dims[0], in_dims[1]); + g = af.convolve2GradientNN(incoming_grad, a, b, c) + display_func(g) + a = af.randu(5, 5, 3) b = af.randu(3, 3, 2) display_func(af.convolve3(a, b)) @@ -106,7 +115,6 @@ def simple_signal(verbose=False): display_func(af.convolve(a, b)) display_func(af.fft_convolve(a, b)) - b = af.randu(3, 1) x = af.randu(10, 1) a = af.randu(2, 1) @@ -117,4 +125,5 @@ def simple_signal(verbose=False): display_func(af.medfilt2(a)) display_func(af.medfilt(a)) -_util.tests['signal'] = simple_signal + +_util.tests["signal"] = simple_signal diff --git a/arrayfire/tests/simple/sparse.py b/tests/simple/sparse.py similarity index 88% rename from arrayfire/tests/simple/sparse.py rename to tests/simple/sparse.py index 89315dfb1..bda87dfb7 100644 --- a/arrayfire/tests/simple/sparse.py +++ b/tests/simple/sparse.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,11 +10,13 @@ ######################################################## import arrayfire as af + from . import _util + def simple_sparse(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) + print_func = _util.print_func(verbose) dd = af.randu(5, 5) ds = dd * (dd > 0.5) @@ -25,4 +28,5 @@ def simple_sparse(verbose=False): print_func(af.sparse_get_nnz(sp)) print_func(af.sparse_get_storage(sp)) -_util.tests['sparse'] = simple_sparse + +_util.tests["sparse"] = simple_sparse diff --git a/arrayfire/tests/simple/statistics.py b/tests/simple/statistics.py similarity index 61% rename from arrayfire/tests/simple/statistics.py rename to tests/simple/statistics.py index d7d5a63d4..39fe6703f 100644 --- a/arrayfire/tests/simple/statistics.py +++ b/tests/simple/statistics.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python + ####################################################### # Copyright (c) 2015, ArrayFire # All rights reserved. @@ -9,11 +10,13 @@ ######################################################## import arrayfire as af + from . import _util + def simple_statistics(verbose=False): display_func = _util.display_func(verbose) - print_func = _util.print_func(verbose) + print_func = _util.print_func(verbose) a = af.randu(5, 5) b = af.randu(5, 5) @@ -25,19 +28,26 @@ def simple_statistics(verbose=False): print_func(af.mean(a, weights=w)) display_func(af.var(a, dim=0)) - display_func(af.var(a, isbiased=True, dim=0)) + display_func(af.var(a, bias=af.VARIANCE.SAMPLE, dim=0)) display_func(af.var(a, weights=w, dim=0)) print_func(af.var(a)) - print_func(af.var(a, isbiased=True)) + print_func(af.var(a, bias=af.VARIANCE.SAMPLE)) print_func(af.var(a, weights=w)) + mean, var = af.meanvar(a, dim=0) + display_func(mean) + display_func(var) + mean, var = af.meanvar(a, weights=w, bias=af.VARIANCE.SAMPLE, dim=0) + display_func(mean) + display_func(var) + display_func(af.stdev(a, dim=0)) print_func(af.stdev(a)) display_func(af.var(a, dim=0)) - display_func(af.var(a, isbiased=True, dim=0)) + display_func(af.var(a, bias=af.VARIANCE.SAMPLE, dim=0)) print_func(af.var(a)) - print_func(af.var(a, isbiased=True)) + print_func(af.var(a, bias=af.VARIANCE.SAMPLE)) display_func(af.median(a, dim=0)) print_func(af.median(w)) @@ -47,10 +57,11 @@ def simple_statistics(verbose=False): data = af.iota(5, 3) k = 3 dim = 0 - order = af.TOPK.DEFAULT # defaults to af.TOPK.MAX - assert(dim == 0) # topk currently supports first dim only - values,indices = af.topk(data, k, dim, order) + order = af.TOPK.DEFAULT # defaults to af.TOPK.MAX + assert(dim == 0) # topk currently supports first dim only + values, indices = af.topk(data, k, dim, order) display_func(values) display_func(indices) -_util.tests['statistics'] = simple_statistics + +_util.tests["statistics"] = simple_statistics