From d75646ab4ed44f679abf31c3ed44cd3d0315cf14 Mon Sep 17 00:00:00 2001
From: Natalia Polina <natalia.polina@intel.com>
Date: Fri, 31 May 2024 13:13:11 -0500
Subject: [PATCH 1/2] Update docs for Universal Functions

---
 .pre-commit-config.yaml                   |   3 +-
 doc/conf.py                               |   8 +-
 doc/reference/ufunc.rst                   |  24 +-
 dpnp/dpnp_algo/dpnp_elementwise_common.py | 423 ++++++++++++++++++++++
 dpnp/dpnp_iface_mathematical.py           |  52 +--
 5 files changed, 469 insertions(+), 41 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b996291c155e..76bebb782a63 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -98,6 +98,7 @@ repos:
             "-sn", # Don't display the score
             "--disable=import-error",
             "--disable=redefined-builtin",
-            "--disable=unused-wildcard-import"
+            "--disable=unused-wildcard-import",
+            "--class-naming-style=snake_case"
             ]
         files: '^dpnp/(dpnp_iface.*|linalg)'
diff --git a/doc/conf.py b/doc/conf.py
index 081070a5e59b..a580d2921338 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -8,7 +8,11 @@
 
 from sphinx.ext.autodoc import FunctionDocumenter
 
-from dpnp.dpnp_algo.dpnp_elementwise_common import DPNPBinaryFunc, DPNPUnaryFunc
+from dpnp.dpnp_algo.dpnp_elementwise_common import (
+    DPNPBinaryFunc,
+    DPNPUnaryFunc,
+    ufunc,
+)
 
 try:
     import comparison_generator
@@ -202,7 +206,7 @@
 
 # -- Options for todo extension ----------------------------------------------
 def _can_document_member(member, *args, **kwargs):
-    if isinstance(member, (DPNPBinaryFunc, DPNPUnaryFunc)):
+    if isinstance(member, (DPNPBinaryFunc, DPNPUnaryFunc, ufunc)):
         return True
     return orig(member, *args, **kwargs)
 
diff --git a/doc/reference/ufunc.rst b/doc/reference/ufunc.rst
index 2dffca15e889..260fc40018bb 100644
--- a/doc/reference/ufunc.rst
+++ b/doc/reference/ufunc.rst
@@ -5,7 +5,29 @@ Universal Functions (ufunc)
 
 .. https://docs.scipy.org/doc/numpy/reference/ufuncs.html
 
-DPNP provides universal functions (a.k.a. ufuncs) to support various element-wise operations.
+ufuncs
+------
+.. autoclass:: dpnp.ufunc
+
+Attributes
+~~~~~~~~~~
+.. autosummary::
+   :toctree: generated/
+   :nosignatures:
+
+   dpnp.ufunc.nin
+   dpnp.ufunc.nout
+   dpnp.ufunc.nargs
+   dpnp.ufunc.types
+   dpnp.ufunc.ntypes
+
+Methods
+~~~~~~~
+.. autosummary::
+   :toctree: generated/
+   :nosignatures:
+
+   dpnp.ufunc.outer
 
 Available ufuncs
 ----------------
diff --git a/dpnp/dpnp_algo/dpnp_elementwise_common.py b/dpnp/dpnp_algo/dpnp_elementwise_common.py
index 374981a63031..d6bf7b67da38 100644
--- a/dpnp/dpnp_algo/dpnp_elementwise_common.py
+++ b/dpnp/dpnp_algo/dpnp_elementwise_common.py
@@ -43,9 +43,432 @@
     "DPNPReal",
     "DPNPRound",
     "DPNPUnaryFunc",
+    "ufunc",
 ]
 
 
+import dpctl.tensor._tensor_elementwise_impl as ti
+
+import dpnp.backend.extensions.vm._vm_impl as vmi
+
+
+class ufunc:
+    """
+    ufunc(name, docs, nin, ...)
+
+    DPNP provides universal functions (a.k.a. ufuncs) to support various
+    element-wise operations.
+
+    Optional keyword arguments
+    --------------------------
+    out : {None, dpnp.ndarray, usm_ndarray}, optional
+        Output array to populate.
+        Array must have the correct shape and the expected data type.
+    order : {None, "C", "F", "A", "K"}, optional
+        Memory layout of the newly output array, Cannot be provided
+        together with `out`. Default: ``"K"``.
+    dtype : {None, dtype}, optional
+        If provided, the destination array will have this dtype. Cannot be
+        provided together with `out`. Default: ``None``.
+    casting : {"no", "equiv", "safe", "same_kind", "unsafe"}, optional
+        Controls what kind of data casting may occur. Cannot be provided
+        together with `out`. Default: ``"safe"``.
+
+    Limitations
+    -----------
+    Keyword arguments `where` and `subok` are supported with their default values.
+    Other keyword arguments is currently unsupported.
+    Otherwise ``NotImplementedError`` exception will be raised.
+
+    """
+
+    def __init__(
+        self,
+        name,
+        docs,
+        nin,
+        mkl_call=False,
+        inplace=False,
+        acceptance_fn=False,
+    ):
+        self.name_ = name
+        self.nin_ = nin
+        _name = "_" + name
+
+        dpt_result_type = getattr(ti, _name + "_result_type")
+        dpt_impl_fn = getattr(ti, _name)
+
+        if nin == 1:
+
+            def _call_func(src, dst, sycl_queue, depends=None):
+                """
+                A callback to register in UnaryElementwiseFunc class of
+                dpctl.tensor
+                """
+
+                if depends is None:
+                    depends = []
+
+                if mkl_call is True:
+                    mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
+                    mkl_impl_fn = getattr(vmi, _name)
+                    if mkl_fn_to_call is not None and mkl_fn_to_call(
+                        sycl_queue, src, dst
+                    ):
+                        # call pybind11 extension for unary function from OneMKL VM
+                        return mkl_impl_fn(sycl_queue, src, dst, depends)
+                return dpt_impl_fn(src, dst, sycl_queue, depends)
+
+            self.func = UnaryElementwiseFunc(
+                name,
+                dpt_result_type,
+                _call_func,
+                docs,
+                acceptance_fn=acceptance_fn,
+            )
+
+        elif nin == 2:
+            if inplace is True:
+                binary_inplace_fn = getattr(ti, _name + "_inplace")
+            else:
+                binary_inplace_fn = None
+
+            def _call_func(src1, src2, dst, sycl_queue, depends=None):
+                """
+                A callback to register in UnaryElementwiseFunc class of
+                dpctl.tensor
+                """
+
+                if depends is None:
+                    depends = []
+
+                if mkl_call is True:
+                    mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
+                    mkl_impl_fn = getattr(vmi, _name)
+                    if mkl_fn_to_call is not None and mkl_fn_to_call(
+                        sycl_queue, src1, src2, dst
+                    ):
+                        # call pybind11 extension for binary function from OneMKL VM
+                        return mkl_impl_fn(sycl_queue, src1, src2, dst, depends)
+                return dpt_impl_fn(src1, src2, dst, sycl_queue, depends)
+
+            self.func = BinaryElementwiseFunc(
+                name,
+                dpt_result_type,
+                _call_func,
+                docs,
+                binary_inplace_fn,
+                acceptance_fn=acceptance_fn,
+            )
+        else:
+            raise NotImplementedError
+
+        self.__doc__ = docs
+        self.__name__ = "ufunc"
+
+    def __call__(
+        self,
+        *args,
+        out=None,
+        where=True,
+        casting="same_kind",
+        order="K",
+        dtype=None,
+        subok=True,
+        **kwargs,
+    ):
+        dpnp.check_supported_arrays_type(
+            *args, scalar_type=True, all_scalars=False
+        )
+        if kwargs:
+            raise NotImplementedError(
+                f"Requested function={self.name_} with kwargs={kwargs} "
+                "isn't currently supported."
+            )
+        if where is not True:
+            raise NotImplementedError(
+                f"Requested function={self.name_} with where={where} "
+                "isn't currently supported."
+            )
+        if subok is not True:
+            raise NotImplementedError(
+                f"Requested function={self.name_} with subok={subok} "
+                "isn't currently supported."
+            )
+        if dtype is not None and out is not None:
+            raise TypeError(
+                f"Requested function={self.name_} only takes `out` or `dtype`"
+                "as an argument, but both were provided."
+            )
+        if order is None:
+            order = "K"
+        elif order in "afkcAFKC":
+            order = order.upper()
+        else:
+            raise ValueError(
+                f"order must be one of 'C', 'F', 'A', or 'K' (got '{order}')"
+            )
+
+        if self.nin_ == 1:
+            if out is None and len(args) == 2:
+                out = args[1]
+            x = args[0]
+            if dtype is not None:
+                x = dpnp.astype(x, dtype=dtype, casting=casting, copy=False)
+            x_usm = dpnp.get_usm_ndarray(x)
+            astype_usm_args = (x_usm,)
+        else:
+            if out is None and len(args) == 3:
+                out = args[2]
+            x1, x2 = args[0], args[1]
+            if dtype is not None:
+                if dpnp.isscalar(x1):
+                    x1 = dpnp.asarray(x1, dtype=dtype)
+                    x2 = dpnp.astype(
+                        x2, dtype=dtype, casting=casting, copy=False
+                    )
+                elif dpnp.isscalar(x2):
+                    x1 = dpnp.astype(
+                        x1, dtype=dtype, casting=casting, copy=False
+                    )
+                    x2 = dpnp.asarray(x2, dtype=dtype)
+                else:
+                    x1 = dpnp.astype(
+                        x1, dtype=dtype, casting=casting, copy=False
+                    )
+                    x2 = dpnp.astype(
+                        x2, dtype=dtype, casting=casting, copy=False
+                    )
+
+            x1_usm = dpnp.get_usm_ndarray_or_scalar(x1)
+            x2_usm = dpnp.get_usm_ndarray_or_scalar(x2)
+            astype_usm_args = x1_usm, x2_usm
+
+        out_usm = None if out is None else dpnp.get_usm_ndarray(out)
+
+        res_usm = self.func.__call__(*astype_usm_args, out=out_usm, order=order)
+
+        if out is not None and isinstance(out, dpnp_array):
+            return out
+        return dpnp_array._create_from_usm_ndarray(res_usm)
+
+    @property
+    def nin(self):
+        """
+        Returns the number of arguments treated as inputs.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> np.add.nin
+        2
+        >>> np.multiply.nin
+        2
+        >>> np.power.nin
+        2
+        >>> np.exp.nin
+        1
+
+        """
+
+        return self.nin_
+
+    @property
+    def nout(self):
+        """
+        Returns the number of arguments treated as outputs.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> np.add.nin
+        1
+        >>> np.multiply.nin
+        1
+        >>> np.power.nin
+        1
+        >>> np.exp.nin
+        1
+
+        """
+
+        return 1
+
+    @property
+    def nargs(self):
+        """
+        Returns the number of arguments treated.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> np.add.nin
+        3
+        >>> np.multiply.nin
+        3
+        >>> np.power.nin
+        3
+        >>> np.exp.nin
+        2
+
+        """
+
+        return self.nin_ + 1
+
+    @property
+    def types(self):
+        """
+        Returns information about types supported by implementation function,
+        using NumPy's character encoding for data types, e.g.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> np.add.types
+        ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I',
+        'll->l', 'LL->L', 'ee->e', 'ff->f', 'dd->d', 'FF->F', 'DD->D']
+
+        >>> np.multiply.types
+        ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I',
+        'll->l', 'LL->L', 'ee->e', 'ff->f', 'dd->d', 'FF->F', 'DD->D']
+
+        >>> np.power.types
+        ['bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l',
+        'LL->L', 'ee->e', 'ff->f', 'dd->d', 'FF->F', 'DD->D']
+
+        >>> np.exp.types
+        ['e->e', 'f->f', 'd->d', 'F->F', 'D->D']
+
+        >>> np.remainder.types
+        ['bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l',
+        'LL->L', 'ee->e', 'ff->f', 'dd->d']
+
+        """
+
+        return self.func.types
+
+    @property
+    def ntypes(self):
+        """
+        The number of types.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> np.add.ntypes
+        14
+        >>> np.multiply.ntypes
+        14
+        >>> np.power.ntypes
+        13
+        >>> np.exp.ntypes
+        5
+        >>> np.remainder.ntypes
+        11
+
+        """
+
+        return len(self.func.types)
+
+    def outer(
+        self,
+        x1,
+        x2,
+        out=None,
+        where=True,
+        order="K",
+        dtype=None,
+        subok=True,
+        **kwargs,
+    ):
+        """
+        Apply the ufunc op to all pairs (a, b) with a in A and b in B.
+
+        Parameters
+        ----------
+        x1 : {dpnp.ndarray, usm_ndarray}
+            First input array.
+        x2 : {dpnp.ndarray, usm_ndarray}
+            Second input array.
+        out : {None, dpnp.ndarray, usm_ndarray}, optional
+            Output array to populate.
+            Array must have the correct shape and the expected data type.
+        **kwargs
+            For other keyword-only arguments, see the :obj:`dpnp.ufunc`.
+
+        Returns
+        -------
+        out : dpnp.ndarray
+            Output array. The data type of the returned array is determined by
+            the Type Promotion Rules.
+
+        Limitations
+        -----------
+        Parameters `where` and `subok` are supported with their default values.
+        Keyword argument `kwargs` is currently unsupported.
+        Otherwise ``NotImplementedError`` exception will be raised.
+
+        See also
+        --------
+        :obj:`dpnp.outer` : A less powerful version of dpnp.multiply.outer
+                            that ravels all inputs to 1D. This exists primarily
+                            for compatibility with old code.
+
+        :obj:`dpnp.tensordot` : dpnp.tensordot(a, b, axes=((), ())) and
+                                dpnp.multiply.outer(a, b) behave same for all
+                                dimensions of a and b.
+
+        Examples
+        --------
+        >>> import dpnp as np
+        >>> A = np.array([1, 2, 3])
+        >>> B = np.array([4, 5, 6])
+        >>> np.multiply.outer(A, B)
+        array([[ 4,  5,  6],
+               [ 8, 10, 12],
+               [12, 15, 18]])
+
+        A multi-dimensional example:
+
+        >>> A = np.array([[1, 2, 3], [4, 5, 6]])
+        >>> A.shape
+        (2, 3)
+        >>> B = np.array([[1, 2, 3, 4]])
+        >>> B.shape
+        (1, 4)
+        >>> C = np.multiply.outer(A, B)
+        >>> C.shape; C
+        (2, 3, 1, 4)
+        array([[[[ 1,  2,  3,  4]],
+                [[ 2,  4,  6,  8]],
+                [[ 3,  6,  9, 12]]],
+               [[[ 4,  8, 12, 16]],
+                [[ 5, 10, 15, 20]],
+                [[ 6, 12, 18, 24]]]])
+
+        """
+
+        dpnp.check_supported_arrays_type(
+            x1, x2, scalar_type=True, all_scalars=False
+        )
+        if dpnp.isscalar(x1) or dpnp.isscalar(x2):
+            _x1 = x1
+            _x2 = x2
+        else:
+            _x1 = x1[(Ellipsis,) + (None,) * x2.ndim]
+            _x2 = x2[(None,) * x1.ndim + (Ellipsis,)]
+        return self.__call__(
+            _x1,
+            _x2,
+            out=out,
+            where=where,
+            order=order,
+            dtype=dtype,
+            subok=subok,
+            **kwargs,
+        )
+
+
 class DPNPUnaryFunc(UnaryElementwiseFunc):
     """
     Class that implements unary element-wise functions.
diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py
index 716696cacbdc..ac74fef47d7f 100644
--- a/dpnp/dpnp_iface_mathematical.py
+++ b/dpnp/dpnp_iface_mathematical.py
@@ -83,6 +83,8 @@
 from .dpnp_utils.dpnp_utils_linearalgebra import dpnp_cross
 from .dpnp_utils.dpnp_utils_reduction import dpnp_wrap_reduction_call
 
+ufunc = dpnp.dpnp_algo.dpnp_elementwise_common.ufunc
+
 __all__ = [
     "abs",
     "absolute",
@@ -130,6 +132,7 @@
     "trapz",
     "true_divide",
     "trunc",
+    "ufunc",
 ]
 
 
@@ -169,6 +172,8 @@ def _get_reduction_res_dt(a, dtype, _out):
 
 
 _ABS_DOCSTRING = """
+abs(x, out, **kwargs)
+
 Calculates the absolute value for each element `x_i` of input array `x`.
 
 For full documentation refer to :obj:`numpy.absolute`.
@@ -177,12 +182,11 @@ def _get_reduction_res_dt(a, dtype, _out):
 ----------
 x : {dpnp.ndarray, usm_ndarray}
     Input array, expected to have numeric data type.
-out : {None, dpnp.ndarray}, optional
+out : {None, dpnp.ndarray, usm_ndarray}, optional
     Output array to populate.
     Array must have the correct shape and the expected data type.
-order : {"C", "F", "A", "K"}, optional
-    Memory layout of the newly output array, if parameter `out` is ``None``.
-    Default: "K".
+**kwargs
+    For other keyword-only arguments, see the :obj:`dpnp.ufunc`.
 
 Returns
 -------
@@ -194,12 +198,6 @@ def _get_reduction_res_dt(a, dtype, _out):
     the returned array has a real-valued floating-point data type whose
     precision matches the precision of `x`.
 
-Limitations
------------
-Parameters `where` and `subok` are supported with their default values.
-Keyword argument `kwargs` is currently unsupported.
-Otherwise ``NotImplementedError`` exception will be raised.
-
 See Also
 --------
 :obj:`dpnp.fabs` : Calculate the absolute value element-wise excluding complex types.
@@ -220,20 +218,15 @@ def _get_reduction_res_dt(a, dtype, _out):
 array(1.5620499351813308)
 """
 
-absolute = DPNPUnaryFunc(
-    "abs",
-    ti._abs_result_type,
-    ti._abs,
-    _ABS_DOCSTRING,
-    mkl_fn_to_call=vmi._mkl_abs_to_call,
-    mkl_impl_fn=vmi._abs,
-)
+absolute = ufunc("abs", _ABS_DOCSTRING, 1, mkl_call=True)
 
 
 abs = absolute
 
 
 _ADD_DOCSTRING = """
+add(x1, x2, out, **kwargs)
+
 Calculates the sum for each element `x1_i` of the input array `x1` with
 the respective element `x2_i` of the input array `x2`.
 
@@ -245,12 +238,11 @@ def _get_reduction_res_dt(a, dtype, _out):
     First input array, expected to have numeric data type.
 x2 : {dpnp.ndarray, usm_ndarray}
     Second input array, also expected to have numeric data type.
-out : {None, dpnp.ndarray}, optional
+out : {None, dpnp.ndarray, usm_ndarray}, optional
     Output array to populate.
     Array must have the correct shape and the expected data type.
-order : {"C", "F", "A", "K"}, optional
-    Memory layout of the newly output array, if parameter `out` is ``None``.
-    Default: "K".
+**kwargs
+    For other keyword-only arguments, see the :obj:`dpnp.ufunc`.
 
 Returns
 -------
@@ -258,12 +250,6 @@ def _get_reduction_res_dt(a, dtype, _out):
     An array containing the element-wise sums. The data type of the
     returned array is determined by the Type Promotion Rules.
 
-Limitations
------------
-Parameters `where` and `subok` are supported with their default values.
-Keyword argument `kwargs` is currently unsupported.
-Otherwise ``NotImplementedError`` exception will be raised.
-
 Notes
 -----
 Equivalent to `x1` + `x2` in terms of array broadcasting.
@@ -293,15 +279,7 @@ def _get_reduction_res_dt(a, dtype, _out):
 """
 
 
-add = DPNPBinaryFunc(
-    "add",
-    ti._add_result_type,
-    ti._add,
-    _ADD_DOCSTRING,
-    mkl_fn_to_call=vmi._mkl_add_to_call,
-    mkl_impl_fn=vmi._add,
-    binary_inplace_fn=ti._add_inplace,
-)
+add = ufunc("add", _ADD_DOCSTRING, 2, mkl_call=True, inplace=True)
 
 
 _ANGLE_DOCSTRING = """

From e986551f71a2f2d6c28a97c0991f63c2322fca8a Mon Sep 17 00:00:00 2001
From: Natalia Polina <natalia.polina@intel.com>
Date: Thu, 6 Jun 2024 12:38:10 -0500
Subject: [PATCH 2/2] Update ufunc doc

---
 doc/conf.py                               |   7 +-
 doc/reference/ufunc.rst                   |  26 +-
 dpnp/dpnp_algo/dpnp_elementwise_common.py | 276 ++++++++++++----------
 dpnp/dpnp_algo/dpnp_elementwise_docs.py   | 166 +++++++++++++
 dpnp/dpnp_iface_mathematical.py           | 108 +--------
 5 files changed, 352 insertions(+), 231 deletions(-)
 create mode 100644 dpnp/dpnp_algo/dpnp_elementwise_docs.py

diff --git a/doc/conf.py b/doc/conf.py
index a580d2921338..0a7dd57a2f33 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -11,7 +11,9 @@
 from dpnp.dpnp_algo.dpnp_elementwise_common import (
     DPNPBinaryFunc,
     DPNPUnaryFunc,
+    binary_ufunc,
     ufunc,
+    unary_ufunc,
 )
 
 try:
@@ -206,7 +208,10 @@
 
 # -- Options for todo extension ----------------------------------------------
 def _can_document_member(member, *args, **kwargs):
-    if isinstance(member, (DPNPBinaryFunc, DPNPUnaryFunc, ufunc)):
+    if isinstance(
+        member,
+        (DPNPBinaryFunc, DPNPUnaryFunc, ufunc, unary_ufunc, binary_ufunc),
+    ):
         return True
     return orig(member, *args, **kwargs)
 
diff --git a/doc/reference/ufunc.rst b/doc/reference/ufunc.rst
index 260fc40018bb..2acb004f03fc 100644
--- a/doc/reference/ufunc.rst
+++ b/doc/reference/ufunc.rst
@@ -5,12 +5,35 @@ Universal Functions (ufunc)
 
 .. https://docs.scipy.org/doc/numpy/reference/ufuncs.html
 
+DPNP provides universal functions (a.k.a. ufuncs) to support various element-wise operations.
+DPNP ufunc supports following features of NumPy’s one:
+
+- Broadcasting
+- Output type determination
+- Casting rules
+
 ufuncs
 ------
-.. autoclass:: dpnp.ufunc
+.. autosummary::
+   :toctree: generated/
+
+   dpnp.ufunc
 
 Attributes
 ~~~~~~~~~~
+
+There are some informational attributes that universal functions
+possess. None of the attributes can be set.
+
+============  =================================================================
+**__doc__**   A docstring for each ufunc. The first part of the docstring is
+              dynamically generated from the number of outputs, the name, and
+              the number of inputs. The second part of the docstring is
+              provided at creation time and stored with the ufunc.
+
+**__name__**  The name of the ufunc.
+============  =================================================================
+
 .. autosummary::
    :toctree: generated/
    :nosignatures:
@@ -25,7 +48,6 @@ Methods
 ~~~~~~~
 .. autosummary::
    :toctree: generated/
-   :nosignatures:
 
    dpnp.ufunc.outer
 
diff --git a/dpnp/dpnp_algo/dpnp_elementwise_common.py b/dpnp/dpnp_algo/dpnp_elementwise_common.py
index d6bf7b67da38..121cc9925fb4 100644
--- a/dpnp/dpnp_algo/dpnp_elementwise_common.py
+++ b/dpnp/dpnp_algo/dpnp_elementwise_common.py
@@ -31,6 +31,7 @@
 )
 
 import dpnp
+import dpnp.dpnp_algo.dpnp_elementwise_docs as ufunc_docs
 from dpnp.dpnp_array import dpnp_array
 
 __all__ = [
@@ -44,6 +45,8 @@
     "DPNPRound",
     "DPNPUnaryFunc",
     "ufunc",
+    "unary_ufunc",
+    "binary_ufunc",
 ]
 
 
@@ -54,13 +57,20 @@
 
 class ufunc:
     """
-    ufunc(name, docs, nin, ...)
+    ufunc()
 
-    DPNP provides universal functions (a.k.a. ufuncs) to support various
-    element-wise operations.
+    Functions that operate element by element on whole arrays.
 
-    Optional keyword arguments
-    --------------------------
+    Calling ufuncs
+    --------------
+    op(*x[, out], **kwargs)
+
+    Apply `op` to the arguments `*x` elementwise, broadcasting the arguments.
+
+    Parameters
+    ----------
+    *x : {dpnp.ndarray, usm_ndarray}
+        Input arrays.
     out : {None, dpnp.ndarray, usm_ndarray}, optional
         Output array to populate.
         Array must have the correct shape and the expected data type.
@@ -85,86 +95,17 @@ class ufunc:
     def __init__(
         self,
         name,
-        docs,
         nin,
-        mkl_call=False,
-        inplace=False,
-        acceptance_fn=False,
+        nout=1,
+        func=None,
+        to_usm_astype=None,
     ):
-        self.name_ = name
         self.nin_ = nin
-        _name = "_" + name
-
-        dpt_result_type = getattr(ti, _name + "_result_type")
-        dpt_impl_fn = getattr(ti, _name)
-
-        if nin == 1:
-
-            def _call_func(src, dst, sycl_queue, depends=None):
-                """
-                A callback to register in UnaryElementwiseFunc class of
-                dpctl.tensor
-                """
-
-                if depends is None:
-                    depends = []
-
-                if mkl_call is True:
-                    mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
-                    mkl_impl_fn = getattr(vmi, _name)
-                    if mkl_fn_to_call is not None and mkl_fn_to_call(
-                        sycl_queue, src, dst
-                    ):
-                        # call pybind11 extension for unary function from OneMKL VM
-                        return mkl_impl_fn(sycl_queue, src, dst, depends)
-                return dpt_impl_fn(src, dst, sycl_queue, depends)
-
-            self.func = UnaryElementwiseFunc(
-                name,
-                dpt_result_type,
-                _call_func,
-                docs,
-                acceptance_fn=acceptance_fn,
-            )
-
-        elif nin == 2:
-            if inplace is True:
-                binary_inplace_fn = getattr(ti, _name + "_inplace")
-            else:
-                binary_inplace_fn = None
-
-            def _call_func(src1, src2, dst, sycl_queue, depends=None):
-                """
-                A callback to register in UnaryElementwiseFunc class of
-                dpctl.tensor
-                """
-
-                if depends is None:
-                    depends = []
-
-                if mkl_call is True:
-                    mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
-                    mkl_impl_fn = getattr(vmi, _name)
-                    if mkl_fn_to_call is not None and mkl_fn_to_call(
-                        sycl_queue, src1, src2, dst
-                    ):
-                        # call pybind11 extension for binary function from OneMKL VM
-                        return mkl_impl_fn(sycl_queue, src1, src2, dst, depends)
-                return dpt_impl_fn(src1, src2, dst, sycl_queue, depends)
-
-            self.func = BinaryElementwiseFunc(
-                name,
-                dpt_result_type,
-                _call_func,
-                docs,
-                binary_inplace_fn,
-                acceptance_fn=acceptance_fn,
-            )
-        else:
-            raise NotImplementedError
-
-        self.__doc__ = docs
-        self.__name__ = "ufunc"
+        self.nout_ = nout
+        self.func = func
+        self.to_usm_astype = to_usm_astype
+        self.__name__ = name
+        self.__doc__ = getattr(ufunc_docs, name + "_docstring")
 
     def __call__(
         self,
@@ -182,23 +123,23 @@ def __call__(
         )
         if kwargs:
             raise NotImplementedError(
-                f"Requested function={self.name_} with kwargs={kwargs} "
+                f"Requested function={self.__name__} with kwargs={kwargs} "
                 "isn't currently supported."
             )
         if where is not True:
             raise NotImplementedError(
-                f"Requested function={self.name_} with where={where} "
+                f"Requested function={self.__name__} with where={where} "
                 "isn't currently supported."
             )
         if subok is not True:
             raise NotImplementedError(
-                f"Requested function={self.name_} with subok={subok} "
+                f"Requested function={self.__name__} with subok={subok} "
                 "isn't currently supported."
             )
-        if dtype is not None and out is not None:
+        if (dtype is not None or casting != "same_kind") and out is not None:
             raise TypeError(
-                f"Requested function={self.name_} only takes `out` or `dtype`"
-                "as an argument, but both were provided."
+                f"Requested function={self.__name__} only takes `out` or "
+                "`dtype` as an argument, but both were provided."
             )
         if order is None:
             order = "K"
@@ -209,40 +150,7 @@ def __call__(
                 f"order must be one of 'C', 'F', 'A', or 'K' (got '{order}')"
             )
 
-        if self.nin_ == 1:
-            if out is None and len(args) == 2:
-                out = args[1]
-            x = args[0]
-            if dtype is not None:
-                x = dpnp.astype(x, dtype=dtype, casting=casting, copy=False)
-            x_usm = dpnp.get_usm_ndarray(x)
-            astype_usm_args = (x_usm,)
-        else:
-            if out is None and len(args) == 3:
-                out = args[2]
-            x1, x2 = args[0], args[1]
-            if dtype is not None:
-                if dpnp.isscalar(x1):
-                    x1 = dpnp.asarray(x1, dtype=dtype)
-                    x2 = dpnp.astype(
-                        x2, dtype=dtype, casting=casting, copy=False
-                    )
-                elif dpnp.isscalar(x2):
-                    x1 = dpnp.astype(
-                        x1, dtype=dtype, casting=casting, copy=False
-                    )
-                    x2 = dpnp.asarray(x2, dtype=dtype)
-                else:
-                    x1 = dpnp.astype(
-                        x1, dtype=dtype, casting=casting, copy=False
-                    )
-                    x2 = dpnp.astype(
-                        x2, dtype=dtype, casting=casting, copy=False
-                    )
-
-            x1_usm = dpnp.get_usm_ndarray_or_scalar(x1)
-            x2_usm = dpnp.get_usm_ndarray_or_scalar(x2)
-            astype_usm_args = x1_usm, x2_usm
+        astype_usm_args = self.to_usm_astype(*args, dtype, casting)
 
         out_usm = None if out is None else dpnp.get_usm_ndarray(out)
 
@@ -292,7 +200,7 @@ def nout(self):
 
         """
 
-        return 1
+        return self.nout_
 
     @property
     def nargs(self):
@@ -313,7 +221,7 @@ def nargs(self):
 
         """
 
-        return self.nin_ + 1
+        return self.nin_ + self.nout_
 
     @property
     def types(self):
@@ -469,6 +377,126 @@ def outer(
         )
 
 
+class unary_ufunc(ufunc):
+    def __init__(
+        self,
+        name,
+        mkl_call=False,
+        acceptance_fn=False,
+    ):
+        def _to_usm_astype(x, dtype, casting):
+            if dtype is not None:
+                x = dpnp.astype(x, dtype=dtype, casting=casting, copy=False)
+            x_usm = dpnp.get_usm_ndarray(x)
+            return (x_usm,)
+
+        _name = "_" + name
+
+        dpt_result_type = getattr(ti, _name + "_result_type")
+        dpt_impl_fn = getattr(ti, _name)
+
+        def _call_func(src, dst, sycl_queue, depends=None):
+            """
+            A callback to register in UnaryElementwiseFunc class of
+            dpctl.tensor
+            """
+
+            if depends is None:
+                depends = []
+
+            if mkl_call is True:
+                mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
+                mkl_impl_fn = getattr(vmi, _name)
+                if mkl_fn_to_call is not None and mkl_fn_to_call(
+                    sycl_queue, src, dst
+                ):
+                    # call pybind11 extension for unary function from OneMKL VM
+                    return mkl_impl_fn(sycl_queue, src, dst, depends)
+            return dpt_impl_fn(src, dst, sycl_queue, depends)
+
+        func = UnaryElementwiseFunc(
+            name,
+            dpt_result_type,
+            _call_func,
+            self.__doc__,
+            acceptance_fn=acceptance_fn,
+        )
+
+        super().__init__(name, nin=1, func=func, to_usm_astype=_to_usm_astype)
+
+
+class binary_ufunc(ufunc):
+    def __init__(
+        self,
+        name,
+        mkl_call=False,
+        inplace=False,
+        acceptance_fn=False,
+    ):
+        def _to_usm_astype(x1, x2, dtype, casting):
+            if dtype is not None:
+                if dpnp.isscalar(x1):
+                    x1 = dpnp.asarray(x1, dtype=dtype)
+                    x2 = dpnp.astype(
+                        x2, dtype=dtype, casting=casting, copy=False
+                    )
+                elif dpnp.isscalar(x2):
+                    x1 = dpnp.astype(
+                        x1, dtype=dtype, casting=casting, copy=False
+                    )
+                    x2 = dpnp.asarray(x2, dtype=dtype)
+                else:
+                    x1 = dpnp.astype(
+                        x1, dtype=dtype, casting=casting, copy=False
+                    )
+                    x2 = dpnp.astype(
+                        x2, dtype=dtype, casting=casting, copy=False
+                    )
+            x1_usm = dpnp.get_usm_ndarray_or_scalar(x1)
+            x2_usm = dpnp.get_usm_ndarray_or_scalar(x2)
+            return x1_usm, x2_usm
+
+        _name = "_" + name
+
+        dpt_result_type = getattr(ti, _name + "_result_type")
+        dpt_impl_fn = getattr(ti, _name)
+
+        if inplace is True:
+            binary_inplace_fn = getattr(ti, _name + "_inplace")
+        else:
+            binary_inplace_fn = None
+
+        def _call_func(src1, src2, dst, sycl_queue, depends=None):
+            """
+            A callback to register in UnaryElementwiseFunc class of
+            dpctl.tensor
+            """
+
+            if depends is None:
+                depends = []
+
+            if mkl_call is True:
+                mkl_fn_to_call = getattr(vmi, "_mkl" + _name + "_to_call")
+                mkl_impl_fn = getattr(vmi, _name)
+                if mkl_fn_to_call is not None and mkl_fn_to_call(
+                    sycl_queue, src1, src2, dst
+                ):
+                    # call pybind11 extension for binary function from OneMKL VM
+                    return mkl_impl_fn(sycl_queue, src1, src2, dst, depends)
+            return dpt_impl_fn(src1, src2, dst, sycl_queue, depends)
+
+        func = BinaryElementwiseFunc(
+            name,
+            dpt_result_type,
+            _call_func,
+            self.__doc__,
+            binary_inplace_fn,
+            acceptance_fn=acceptance_fn,
+        )
+
+        super().__init__(name, nin=2, func=func, to_usm_astype=_to_usm_astype)
+
+
 class DPNPUnaryFunc(UnaryElementwiseFunc):
     """
     Class that implements unary element-wise functions.
diff --git a/dpnp/dpnp_algo/dpnp_elementwise_docs.py b/dpnp/dpnp_algo/dpnp_elementwise_docs.py
new file mode 100644
index 000000000000..6d8195bb283f
--- /dev/null
+++ b/dpnp/dpnp_algo/dpnp_elementwise_docs.py
@@ -0,0 +1,166 @@
+_unary_doc_template = """
+dpnp.%s(x, out=None, order='K', dtype=None, casting="same_kind", **kwargs)
+
+%s
+
+For full documentation refer to :obj:`numpy.%s`.
+
+Parameters
+----------
+x : {dpnp.ndarray, usm_ndarray}
+    Input arrays, expected to have %s data type.
+out : {None, dpnp.ndarray, usm_ndarray}, optional
+    Output array to populate.
+    Array must have the correct shape and the expected data type.
+order : {None, "C", "F", "A", "K"}, optional
+    Memory layout of the newly output array, Cannot be provided
+    together with `out`. Default: ``"K"``.
+dtype : {None, dtype}, optional
+    If provided, the destination array will have this dtype. Cannot be
+    provided together with `out`. Default: ``None``.
+casting : {"no", "equiv", "safe", "same_kind", "unsafe"}, optional
+    Controls what kind of data casting may occur. Cannot be provided
+    together with `out`. Default: ``"safe"``.
+
+Returns
+-------
+out : dpnp.ndarray
+%s
+
+Limitations
+-----------
+Keyword arguments `where` and `subok` are supported with their default values.
+Other keyword arguments is currently unsupported.
+Otherwise ``NotImplementedError`` exception will be raised.
+
+%s
+"""
+
+_binary_doc_template = """
+dpnp.%s(x1, x2, out=None, order='K', dtype=None, casting="same_kind", **kwargs)
+
+%s
+
+For full documentation refer to :obj:`numpy.%s`.
+
+Parameters
+----------
+x1, x2 : {dpnp.ndarray, usm_ndarray}
+    Input arrays, expected to have %s data type.
+out : {None, dpnp.ndarray, usm_ndarray}, optional
+    Output array to populate.
+    Array must have the correct shape and the expected data type.
+order : {None, "C", "F", "A", "K"}, optional
+    Memory layout of the newly output array, Cannot be provided
+    together with `out`. Default: ``"K"``.
+dtype : {None, dtype}, optional
+    If provided, the destination array will have this dtype. Cannot be
+    provided together with `out`. Default: ``None``.
+casting : {"no", "equiv", "safe", "same_kind", "unsafe"}, optional
+    Controls what kind of data casting may occur. Cannot be provided
+    together with `out`. Default: ``"safe"``.
+
+Returns
+-------
+out : dpnp.ndarray
+%s
+
+Limitations
+-----------
+Keyword arguments `where` and `subok` are supported with their default values.
+Other keyword arguments is currently unsupported.
+Otherwise ``NotImplementedError`` exception will be raised.
+
+%s
+"""
+
+
+name = "absolute"
+dtypes = "numeric"
+summary = """
+Calculates the absolute value for each element `x_i` of input array `x`.
+"""
+returns = """
+    An array containing the element-wise absolute values.
+    For complex input, the absolute value is its magnitude.
+    If `x` has a real-valued data type, the returned array has the
+    same data type as `x`. If `x` has a complex floating-point data type,
+    the returned array has a real-valued floating-point data type whose
+    precision matches the precision of `x`.
+"""
+other = """
+See Also
+--------
+:obj:`dpnp.fabs` : Calculate the absolute value element-wise excluding complex types.
+
+Notes
+-----
+``dpnp.abs`` is a shorthand for this function.
+
+Examples
+--------
+>>> import dpnp as np
+>>> a = np.array([-1.2, 1.2])
+>>> np.absolute(a)
+array([1.2, 1.2])
+
+>>> a = np.array(1.2 + 1j)
+>>> np.absolute(a)
+array(1.5620499351813308)
+"""
+abs_docstring = _unary_doc_template % (
+    name,
+    summary,
+    name,
+    dtypes,
+    returns,
+    other,
+)
+
+
+name = "add"
+dtypes = "numeric"
+summary = """
+Calculates the sum for each element `x1_i` of the input array `x1` with the
+respective element `x2_i` of the input array `x2`.
+"""
+returns = """
+    An array containing the element-wise sums. The data type of the returned
+    array is determined by the Type Promotion Rules.
+"""
+other = """
+Notes
+-----
+Equivalent to `x1` + `x2` in terms of array broadcasting.
+
+Examples
+--------
+>>> import dpnp as np
+>>> a = np.array([1, 2, 3])
+>>> b = np.array([1, 2, 3])
+>>> np.add(a, b)
+array([2, 4, 6])
+
+>>> x1 = np.arange(9.0).reshape((3, 3))
+>>> x2 = np.arange(3.0)
+>>> np.add(x1, x2)
+array([[  0.,   2.,   4.],
+       [  3.,   5.,   7.],
+       [  6.,   8.,  10.]])
+
+The ``+`` operator can be used as a shorthand for ``add`` on
+:class:`dpnp.ndarray`.
+
+>>> x1 + x2
+array([[  0.,   2.,   4.],
+       [  3.,   5.,   7.],
+       [  6.,   8.,  10.]])
+"""
+add_docstring = _binary_doc_template % (
+    name,
+    summary,
+    name,
+    dtypes,
+    returns,
+    other,
+)
diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py
index ac74fef47d7f..ba05ecc91c18 100644
--- a/dpnp/dpnp_iface_mathematical.py
+++ b/dpnp/dpnp_iface_mathematical.py
@@ -77,6 +77,8 @@
     acceptance_fn_positive,
     acceptance_fn_sign,
     acceptance_fn_subtract,
+    binary_ufunc,
+    unary_ufunc,
 )
 from .dpnp_array import dpnp_array
 from .dpnp_utils import call_origin, get_usm_allocations
@@ -171,115 +173,13 @@ def _get_reduction_res_dt(a, dtype, _out):
     return dtu._to_device_supported_dtype(dtype, a.sycl_device)
 
 
-_ABS_DOCSTRING = """
-abs(x, out, **kwargs)
-
-Calculates the absolute value for each element `x_i` of input array `x`.
-
-For full documentation refer to :obj:`numpy.absolute`.
-
-Parameters
-----------
-x : {dpnp.ndarray, usm_ndarray}
-    Input array, expected to have numeric data type.
-out : {None, dpnp.ndarray, usm_ndarray}, optional
-    Output array to populate.
-    Array must have the correct shape and the expected data type.
-**kwargs
-    For other keyword-only arguments, see the :obj:`dpnp.ufunc`.
-
-Returns
--------
-out : dpnp.ndarray
-    An array containing the element-wise absolute values.
-    For complex input, the absolute value is its magnitude.
-    If `x` has a real-valued data type, the returned array has the
-    same data type as `x`. If `x` has a complex floating-point data type,
-    the returned array has a real-valued floating-point data type whose
-    precision matches the precision of `x`.
-
-See Also
---------
-:obj:`dpnp.fabs` : Calculate the absolute value element-wise excluding complex types.
-
-Notes
------
-``dpnp.abs`` is a shorthand for this function.
-
-Examples
---------
->>> import dpnp as np
->>> a = np.array([-1.2, 1.2])
->>> np.absolute(a)
-array([1.2, 1.2])
-
->>> a = np.array(1.2 + 1j)
->>> np.absolute(a)
-array(1.5620499351813308)
-"""
-
-absolute = ufunc("abs", _ABS_DOCSTRING, 1, mkl_call=True)
+absolute = unary_ufunc("abs", mkl_call=True)
 
 
 abs = absolute
 
 
-_ADD_DOCSTRING = """
-add(x1, x2, out, **kwargs)
-
-Calculates the sum for each element `x1_i` of the input array `x1` with
-the respective element `x2_i` of the input array `x2`.
-
-For full documentation refer to :obj:`numpy.add`.
-
-Parameters
-----------
-x1 : {dpnp.ndarray, usm_ndarray}
-    First input array, expected to have numeric data type.
-x2 : {dpnp.ndarray, usm_ndarray}
-    Second input array, also expected to have numeric data type.
-out : {None, dpnp.ndarray, usm_ndarray}, optional
-    Output array to populate.
-    Array must have the correct shape and the expected data type.
-**kwargs
-    For other keyword-only arguments, see the :obj:`dpnp.ufunc`.
-
-Returns
--------
-out : dpnp.ndarray
-    An array containing the element-wise sums. The data type of the
-    returned array is determined by the Type Promotion Rules.
-
-Notes
------
-Equivalent to `x1` + `x2` in terms of array broadcasting.
-
-Examples
---------
->>> import dpnp as np
->>> a = np.array([1, 2, 3])
->>> b = np.array([1, 2, 3])
->>> np.add(a, b)
-array([2, 4, 6])
-
->>> x1 = np.arange(9.0).reshape((3, 3))
->>> x2 = np.arange(3.0)
->>> np.add(x1, x2)
-array([[  0.,   2.,   4.],
-       [  3.,   5.,   7.],
-       [  6.,   8.,  10.]])
-
-The ``+`` operator can be used as a shorthand for ``add`` on
-:class:`dpnp.ndarray`.
-
->>> x1 + x2
-array([[  0.,   2.,   4.],
-       [  3.,   5.,   7.],
-       [  6.,   8.,  10.]])
-"""
-
-
-add = ufunc("add", _ADD_DOCSTRING, 2, mkl_call=True, inplace=True)
+add = binary_ufunc("add", mkl_call=True, inplace=True)
 
 
 _ANGLE_DOCSTRING = """