Repository: modin-project/modin
Pull request: FEAT-#5753: Add math functions necessary for picoGPT
<!--
Thank you for your contribution!
Please review the contributing docs: https://modin.readthedocs.io/en/latest/development/contributing.html
if you have questions about contributing.
-->

## What do these changes do?

<!-- Please give a short brief about these changes. -->

- [x] first commit message and PR title follow format outlined [here](https://modin.readthedocs.io/en/latest/development/contributing.html#commit-message-formatting)
  > **_NOTE:_**  If you edit the PR title to match this format, you need to add another commit (even if it's empty) or amend your last commit for the CI job that checks the PR title to pick up the new PR title.
- [ ] passes `flake8 modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [ ] passes `black --check modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [ ] signed commit with `git commit -s` <!-- you can amend your commit with a signature via `git commit -amend -s` -->
- [ ] Resolves #5753 ? <!-- issue must be created for each patch -->
- [ ] tests added and passing
- [ ] module layout described at `docs/development/architecture.rst` is up-to-date <!-- if you have added, renamed or removed files or directories please update the documentation accordingly -->


Problem statement:
Add math for picoGPT on NumPy
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. What kind of performance improvements would you like to see with this new API?
Add the math operations to allow us to use picoGPT.

Hints from issue discussion:

Commit message: lint

Signed-off-by: Rehan Durrani <rehan@ponder.io>

Patch to review:
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
index bcde735a831..c1968849934 100644
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -1493,6 +1493,9 @@ def isin_func(df, values):
         dtypes=np.bool_,
     )
     _logical_not = Map.register(np.logical_not, dtypes=np.bool_)  # Needed for numpy API
+    tanh = Map.register(
+        lambda df, *args, **kwargs: pandas.DataFrame(np.tanh(df, *args, **kwargs))
+    )  # Needed for numpy API
     negative = Map.register(pandas.DataFrame.__neg__)
     notna = Map.register(pandas.DataFrame.notna, dtypes=np.bool_)
     round = Map.register(pandas.DataFrame.round)
diff --git a/modin/numpy/__init__.py b/modin/numpy/__init__.py
index 68a5c78ba3c..5d70723b569 100644
--- a/modin/numpy/__init__.py
+++ b/modin/numpy/__init__.py
@@ -70,6 +70,15 @@
     minimum,
     amin,
     min,
+    sqrt,
+    exp,
+    argmax,
+    argmin,
+    var,
+)
+
+from .trigonometry import (
+    tanh,
 )
 
 from .constants import (
@@ -103,6 +112,18 @@ def where(condition, x=None, y=None):
     )
 
 
+def split(arr, indices, axis=0):
+    return arr.split(indices, axis)
+
+
+def hstack(tup, dtype=None, casting="same_kind"):
+    return tup[0].hstack(tup[1:], dtype, casting)
+
+
+def append(arr, values, axis=None):
+    return arr.append(values, axis)
+
+
 __all__ = [  # noqa: F405
     "array",
     "zeros_like",
@@ -168,4 +189,11 @@ def where(condition, x=None, y=None):
     "nan",
     "newaxis",
     "pi",
+    "sqrt",
+    "tanh",
+    "exp",
+    "argmax",
+    "argmin",
+    "var",
+    "split",
 ]
diff --git a/modin/numpy/arr.py b/modin/numpy/arr.py
index b179520fb32..e835a757e7f 100644
--- a/modin/numpy/arr.py
+++ b/modin/numpy/arr.py
@@ -401,6 +401,13 @@ def max(
         self, axis=None, dtype=None, out=None, keepdims=None, initial=None, where=True
     ):
         check_kwargs(keepdims=keepdims, where=where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         truthy_where = bool(where)
         if initial is None and where is not True:
             raise ValueError(
@@ -458,6 +465,8 @@ def max(
                 else:
                     return array([[initial]])
             return result if truthy_where else initial
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         target = where.where(self, initial) if isinstance(where, array) else self
         result = target._query_compiler.max(axis=axis)
         new_ndim = self._ndim - 1 if not keepdims else self._ndim
@@ -488,6 +497,13 @@ def min(
     ):
         check_kwargs(keepdims=keepdims, where=where)
         truthy_where = bool(where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         if initial is None and where is not True:
             raise ValueError(
                 "reduction operation 'minimum' does not have an identity, so to use a where mask one has to specify 'initial'"
@@ -544,6 +560,8 @@ def min(
                 else:
                     return array([[initial]])
             return result if truthy_where else initial
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         target = where.where(self, initial) if isinstance(where, array) else self
         result = target._query_compiler.min(axis=axis)
         new_ndim = self._ndim - 1 if not keepdims else self._ndim
@@ -630,10 +648,12 @@ def _binary_op(self, other):
             if self.shape != other.shape:
                 # In this case, we either have two mismatched objects trying to do an operation
                 # or a nested 1D object that must be broadcasted trying to do an operation.
+                broadcast = True
                 if self.shape[0] == other.shape[0]:
                     matched_dimension = 0
                 elif self.shape[1] == other.shape[1]:
                     matched_dimension = 1
+                    broadcast = False
                 else:
                     raise ValueError(
                         f"operands could not be broadcast together with shapes {self.shape} {other.shape}"
@@ -642,7 +662,12 @@ def _binary_op(self, other):
                     self.shape[matched_dimension ^ 1] == 1
                     or other.shape[matched_dimension ^ 1] == 1
                 ):
-                    return (self, other, self._ndim, {"broadcast": True, "axis": 1})
+                    return (
+                        self,
+                        other,
+                        self._ndim,
+                        {"broadcast": broadcast, "axis": matched_dimension},
+                    )
                 else:
                     raise ValueError(
                         f"operands could not be broadcast together with shapes {self.shape} {other.shape}"
@@ -806,6 +831,340 @@ def _not_equal(
     def __ne__(self, x2):
         return self._not_equal(x2)
 
+    def _unary_math_operator(
+        self,
+        opName,
+        *args,
+        out=None,
+        where=True,
+        casting="same_kind",
+        order="K",
+        dtype=None,
+        subok=True,
+    ):
+        out_dtype = (
+            dtype
+            if dtype is not None
+            else (out.dtype if out is not None else self.dtype)
+        )
+        check_kwargs(order=order, casting=casting, subok=subok, where=where)
+        result = self._query_compiler.astype(
+            {col_name: out_dtype for col_name in self._query_compiler.columns}
+        )
+        result = getattr(result, opName)(*args)
+        if dtype is not None:
+            result = result.astype({col_name: dtype for col_name in result.columns})
+        if out is not None:
+            out = try_convert_from_interoperable_type(out)
+            check_can_broadcast_to_output(self, out)
+            out._query_compiler = result
+            return out
+        return array(_query_compiler=result, _ndim=self._ndim)
+
+    def tanh(
+        self,
+        out=None,
+        where=True,
+        casting="same_kind",
+        order="K",
+        dtype=None,
+        subok=True,
+    ):
+        return self._unary_math_operator(
+            "tanh",
+            out=out,
+            where=where,
+            casting=casting,
+            order=order,
+            dtype=dtype,
+            subok=subok,
+        )
+
+    def exp(
+        self,
+        out=None,
+        where=True,
+        casting="same_kind",
+        order="K",
+        dtype=None,
+        subok=True,
+    ):
+        return self._unary_math_operator(
+            "rpow",
+            numpy.e,
+            out=out,
+            where=where,
+            casting=casting,
+            order=order,
+            dtype=dtype,
+            subok=subok,
+        )
+
+    def append(self, values, axis=None):
+        if axis is None:
+            return self.flatten().hstack([values.flatten()])
+        elif self._ndim == 1:
+            if values._ndim == 1:
+                return self.hstack([values])
+            raise ValueError(
+                f"all the input arrays must have same number of dimensions, but the array at index 0 has 1 dimension(s) and the array at index 1 has {values._ndim} dimension(s)"
+            )
+        if self.shape[axis ^ 1] != values.shape[axis ^ 1]:
+            raise ValueError(
+                f"all the input array dimensions except for the concatenation axis must match exactly, but along dimension {axis ^ 1}, the array at index 0 has size {self.shape[axis^1]} and the array at index 1 has size {values.shape[axis^1]}"
+            )
+        new_qc = self._query_compiler.concat(axis, values._query_compiler)
+        return array(_query_compiler=new_qc, _ndim=self._ndim)
+
+    def hstack(self, others, dtype=None, casting="same_kind"):
+        check_kwargs(casting=casting)
+        new_dtype = (
+            dtype
+            if dtype is not None
+            else pandas.core.dtypes.cast.find_common_type(
+                [self.dtype] + [a.dtype for a in others]
+            )
+        )
+        for index, i in enumerate([a._ndim for a in others]):
+            if i != self._ndim:
+                raise ValueError(
+                    f"all the input arrays must have same number of dimensions, but the array at index 0 has {self._ndim} dimension(s) and the array at index {index} has {i} dimension(s)"
+                )
+        if self._ndim == 1:
+            new_qc = self._query_compiler.concat(0, [o._query_compiler for o in others])
+        else:
+            for index, i in enumerate([a.shape[0] for a in others]):
+                if i != self.shape[0]:
+                    raise ValueError(
+                        f"all the input array dimensions except for the concatenation axis must match exactly, but along dimension 0, the array at index 0 has size {self.shape[0]} and the array at index {index} has size {i}"
+                    )
+            new_qc = self._query_compiler.concat(1, [o._query_compiler for o in others])
+        return array(_query_compiler=new_qc, _ndim=self._ndim, dtype=new_dtype)
+
+    def split(self, indices, axis=0):
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise IndexError()
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise IndexError
+            axis = new_axis
+        if self._ndim == 1:
+            if axis != 0:
+                raise IndexError
+        if self._ndim == 2:
+            if axis > 1:
+                raise IndexError
+        arrays = []
+        if is_list_like(indices) or isinstance(indices, array):
+            if not isinstance(indices, array):
+                indices = array(indices)
+            if indices._ndim != 1:
+                raise TypeError(
+                    "only integer scalar arrays can be converted to a scalar index"
+                )
+            prev_index = 0
+            for i in range(len(indices) + 1):
+                if i < len(indices):
+                    end_index = indices._query_compiler.take_2d_positional(
+                        [i]
+                    ).to_numpy()[0, 0]
+                    if end_index == 0:
+                        ErrorMessage.single_warning(
+                            "Defaulting to NumPy for empty arrays."
+                        )
+                        new_shape = list(self.shape)
+                        new_shape[axis] = 0
+                        arrays.append(numpy.empty(new_shape, dtype=self.dtype))
+                        continue
+                    if end_index < 0:
+                        end_index = self.shape[axis] + end_index
+                else:
+                    end_index = self.shape[axis]
+                if prev_index > self.shape[axis] or prev_index == end_index:
+                    ErrorMessage.single_warning("Defaulting to NumPy for empty arrays.")
+                    new_shape = list(self.shape)
+                    new_shape[axis] = 0
+                    arrays.append(numpy.empty(new_shape, dtype=self.dtype))
+                else:
+                    idxs = list(range(prev_index, min(end_index, self.shape[axis])))
+                    if axis == 0:
+                        new_qc = self._query_compiler.take_2d_positional(index=idxs)
+                    else:
+                        new_qc = self._query_compiler.take_2d_positional(columns=idxs)
+                    arrays.append(array(_query_compiler=new_qc, _ndim=self._ndim))
+                prev_index = end_index
+        else:
+            if self.shape[axis] % indices != 0:
+                raise ValueError("array split does not result in an equal division")
+            for i in range(0, self.shape[axis], self.shape[axis] // indices):
+                if axis == 0:
+                    new_qc = self._query_compiler.take_2d_positional(
+                        index=list(range(i, i + self.shape[axis] // indices))
+                    )
+                else:
+                    new_qc = self._query_compiler.take_2d_positional(
+                        columns=list(range(i, i + self.shape[axis] // indices))
+                    )
+                arrays.append(array(_query_compiler=new_qc, _ndim=self._ndim))
+        return arrays
+
+    def _compute_masked_variance(self, mask, output_dtype, axis, ddof):
+        if axis == 0 and self._ndim != 1:
+            # Our broadcasting is wrong, so we can't do the final subtraction at the end.
+            raise NotImplementedError(
+                "Masked variance on 2D arrays along axis = 0 is currently unsupported."
+            )
+        axis_mean = self.mean(axis, output_dtype, keepdims=True, where=mask)
+        target = mask.where(self, numpy.nan)
+        if self._ndim == 1:
+            axis_mean = axis_mean._to_numpy()[0]
+            target = target._query_compiler.sub(axis_mean).pow(2).sum(axis=axis)
+        else:
+            target = (target - axis_mean)._query_compiler.pow(2).sum(axis=axis)
+        num_elems = (
+            mask.where(self, 0)._query_compiler.notna().sum(axis=axis, skipna=False)
+        )
+        num_elems = num_elems.sub(ddof)
+        target = target.truediv(num_elems)
+        na_propagation_mask = mask.where(self, 0)._query_compiler.sum(
+            axis=axis, skipna=False
+        )
+        target = target.where(na_propagation_mask.notna(), numpy.nan)
+        return target
+
+    def var(
+        self, axis=None, dtype=None, out=None, ddof=0, keepdims=None, *, where=True
+    ):
+        out_dtype = (
+            dtype
+            if dtype is not None
+            else (out.dtype if out is not None else self.dtype)
+        )
+        out_type = getattr(out_dtype, "type", out_dtype)
+        if isinstance(where, array) and issubclass(out_type, numpy.integer):
+            out_dtype = numpy.float64
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
+        check_kwargs(keepdims=keepdims, where=where)
+        truthy_where = bool(where)
+        if self._ndim == 1:
+            if axis == 1:
+                raise numpy.AxisError(1, 1)
+            if isinstance(where, array):
+                result = self._compute_masked_variance(where, out_dtype, 0, ddof)
+            else:
+                result = self._query_compiler.var(axis=0, skipna=False, ddof=ddof)
+            if keepdims:
+                if out is not None and out.shape != (1,):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 0, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                if out is not None:
+                    out._query_compiler = (
+                        numpy.ones_like(out) * numpy.nan
+                    )._query_compiler
+                if truthy_where or out is not None:
+                    return fix_dtypes_and_determine_return(
+                        result, 1, dtype, out, truthy_where
+                    )
+                else:
+                    return array([numpy.nan], dtype=out_dtype)
+        if axis is None:
+            # If any of the (non-masked) elements of our array are `NaN`, we know that the
+            # result of `mean` must be `NaN`. This is a fastpath to see if any unmasked elements
+            # are `NaN`.
+            contains_na_check = (
+                where.where(self, 0) if isinstance(where, array) else self
+            )
+            if (
+                contains_na_check._query_compiler.isna()
+                .any(axis=1)
+                .any(axis=0)
+                .to_numpy()[0, 0]
+            ):
+                return numpy.nan
+            result = where.where(self, numpy.nan) if isinstance(where, array) else self
+            # Since our current QueryCompiler does not have a variance that reduces 2D objects to
+            # a single value, we need to calculate the variance ourselves. First though, we need
+            # to figure out how many objects that we are taking the variance over (since any
+            # entries in our array that are `numpy.nan` must be ignored when taking the variance,
+            # and so cannot be included in the final division (of the sum over num total elements))
+            num_na_elements = (
+                result._query_compiler.isna().sum(axis=1).sum(axis=0).to_numpy()[0, 0]
+            )
+            num_total_elements = prod(self.shape) - num_na_elements
+            mean = (
+                numpy.array(
+                    [result._query_compiler.sum(axis=1).sum(axis=0).to_numpy()[0, 0]],
+                    dtype=out_dtype,
+                )
+                / num_total_elements
+            )[0]
+            result = (
+                numpy.array(
+                    [
+                        result._query_compiler.sub(mean)
+                        .pow(2)
+                        .sum(axis=1)
+                        .sum(axis=0)
+                        .to_numpy()[0, 0]
+                    ],
+                    dtype=out_dtype,
+                )
+                / (num_total_elements - ddof)
+            )[0]
+            if keepdims:
+                if out is not None and out.shape != (1, 1):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 1, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                if out is not None:
+                    out._query_compiler = (
+                        numpy.ones_like(out) * numpy.nan
+                    )._query_compiler
+                if truthy_where or out is not None:
+                    return fix_dtypes_and_determine_return(
+                        array(numpy.array([[result]]))
+                        .astype(out_dtype)
+                        ._query_compiler,
+                        2,
+                        dtype,
+                        out,
+                        truthy_where,
+                    )
+                else:
+                    return array([[numpy.nan]], dtype=out_dtype)
+            return result if truthy_where else numpy.nan
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
+        if isinstance(where, array):
+            result = self._compute_masked_variance(where, out_dtype, axis, ddof)
+        else:
+            result = self._query_compiler.astype(
+                {col_name: out_dtype for col_name in self._query_compiler.columns}
+            ).var(axis=axis, skipna=False)
+        new_ndim = self._ndim - 1 if not keepdims else self._ndim
+        if new_ndim == 0:
+            return result.to_numpy()[0, 0] if truthy_where else numpy.nan
+        if not keepdims and axis != 1:
+            result = result.transpose()
+        if out is not None:
+            out._query_compiler = (numpy.ones_like(out) * numpy.nan)._query_compiler
+        if truthy_where or out is not None:
+            return fix_dtypes_and_determine_return(
+                result, new_ndim, dtype, out, truthy_where
+            )
+        else:
+            return (
+                numpy.ones(array(_query_compiler=result, _ndim=new_ndim).shape)
+            ) * numpy.nan
+
     def _compute_masked_mean(self, mask, output_dtype, axis):
         # By default, pandas ignores NaN values when doing computations.
         # NumPy; however, propagates the value by default. We use pandas
@@ -833,6 +1192,13 @@ def mean(self, axis=None, dtype=None, out=None, keepdims=None, *, where=True):
         out_type = getattr(out_dtype, "type", out_dtype)
         if isinstance(where, array) and issubclass(out_type, numpy.integer):
             out_dtype = numpy.float64
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         check_kwargs(keepdims=keepdims, where=where)
         truthy_where = bool(where)
         if self._ndim == 1:
@@ -917,6 +1283,8 @@ def mean(self, axis=None, dtype=None, out=None, keepdims=None, *, where=True):
                 else:
                     return array([[numpy.nan]], dtype=out_dtype)
             return result if truthy_where else numpy.nan
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         if isinstance(where, array):
             result = self._compute_masked_mean(where, out_dtype, axis)
         else:
@@ -1207,6 +1575,13 @@ def prod(
         )
         initial = 1 if initial is None else initial
         check_kwargs(keepdims=keepdims, where=where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         truthy_where = bool(where)
         if self._ndim == 1:
             if axis == 1:
@@ -1269,6 +1644,8 @@ def prod(
                 else:
                     return array([[initial]], dtype=out_dtype)
             return result if truthy_where else initial
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         target = where.where(self, 1) if isinstance(where, array) else self
         result = target._query_compiler.astype(
             {col_name: out_dtype for col_name in target._query_compiler.columns}
@@ -1510,6 +1887,13 @@ def sum(
         )
         initial = 0 if initial is None else initial
         check_kwargs(keepdims=keepdims, where=where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         truthy_where = bool(where)
         if self._ndim == 1:
             if axis == 1:
@@ -1570,6 +1954,8 @@ def sum(
                 else:
                     return array([[initial]], dtype=out_dtype)
             return result if truthy_where else initial
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         target = where.where(self, 0) if isinstance(where, array) else self
         result = target._query_compiler.astype(
             {col_name: out_dtype for col_name in target._query_compiler.columns}
@@ -1597,6 +1983,13 @@ def sum(
     def all(self, axis=None, out=None, keepdims=None, *, where=True):
         check_kwargs(keepdims=keepdims, where=where)
         truthy_where = bool(where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         target = where.where(self, True) if isinstance(where, array) else self
         if self._ndim == 1:
             if axis == 1:
@@ -1632,6 +2025,8 @@ def all(self, axis=None, out=None, keepdims=None, *, where=True):
                 else:
                     return array([[True]], dtype=bool)
             return result.to_numpy()[0, 0] if truthy_where else True
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         result = target._query_compiler.all(axis=axis)
         new_ndim = self._ndim - 1 if not keepdims else self._ndim
         if new_ndim == 0:
@@ -1651,6 +2046,13 @@ def all(self, axis=None, out=None, keepdims=None, *, where=True):
     def any(self, axis=None, out=None, keepdims=None, *, where=True):
         check_kwargs(keepdims=keepdims, where=where)
         truthy_where = bool(where)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
         target = where.where(self, False) if isinstance(where, array) else self
         if self._ndim == 1:
             if axis == 1:
@@ -1686,6 +2088,8 @@ def any(self, axis=None, out=None, keepdims=None, *, where=True):
                 else:
                     return array([[False]], dtype=bool)
             return result.to_numpy()[0, 0] if truthy_where else False
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
         result = target._query_compiler.any(axis=axis)
         new_ndim = self._ndim - 1 if not keepdims else self._ndim
         if new_ndim == 0:
@@ -1702,6 +2106,144 @@ def any(self, axis=None, out=None, keepdims=None, *, where=True):
 
     _any = any
 
+    def argmax(self, axis=None, out=None, keepdims=None):
+        check_kwargs(keepdims=keepdims)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
+        if self._ndim == 1:
+            if axis == 1:
+                raise numpy.AxisError(1, 1)
+            if self._query_compiler.isna().any(axis=1).any(axis=0).to_numpy()[0, 0]:
+                na_row_map = self._query_compiler.isna().any(axis=1)
+                result = na_row_map.idxmax().to_numpy()[0, 0]
+            else:
+                result = self._query_compiler.idxmax(axis=0)
+            if keepdims:
+                if out is not None and out.shape != (1,):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 0, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                return fix_dtypes_and_determine_return(
+                    result, 1, numpy.int64, out, True
+                )
+            return result.to_numpy()[0, 0]
+        if axis is None:
+            if self._query_compiler.isna().any(axis=1).any(axis=0).to_numpy()[0, 0]:
+                na_row_map = self._query_compiler.isna().any(axis=1)
+                na_row = self._query_compiler.getitem_array(na_row_map)
+                col_idx = na_row.to_numpy().argmax()
+                final_idxmax = na_row_map.idxmax().to_numpy().flatten()
+            else:
+                inner_idxs = self._query_compiler.idxmax(axis=1)
+                final_idxmax = (
+                    self._query_compiler.max(axis=1).idxmax(axis=0).to_numpy().flatten()
+                )
+                col_idx = inner_idxs.take_2d_positional(final_idxmax, [0]).to_numpy()[
+                    0, 0
+                ]
+            result = (self.shape[1] * final_idxmax[0]) + col_idx
+            if keepdims:
+                if out is not None and out.shape != (1, 1):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 0, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                return fix_dtypes_and_determine_return(
+                    array(numpy.array([[result]], dtype=bool))._query_compiler,
+                    2,
+                    numpy.int64,
+                    out,
+                    True,
+                )
+            return result
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
+        result = self._query_compiler.idxmax(axis=axis)
+        na_mask = self._query_compiler.isna().any(axis=axis)
+        if na_mask.any(axis=axis ^ 1).to_numpy()[0, 0]:
+            na_idxs = self._query_compiler.isna().idxmax(axis=axis)
+            result = na_mask.where(na_idxs, result)
+        new_ndim = self._ndim - 1 if not keepdims else self._ndim
+        if new_ndim == 0:
+            result = result.to_numpy()[0, 0]
+            return result
+        if not keepdims and axis != 1:
+            result = result.transpose()
+        return fix_dtypes_and_determine_return(result, new_ndim, numpy.int64, out, True)
+
+    def argmin(self, axis=None, out=None, keepdims=None):
+        check_kwargs(keepdims=keepdims)
+        if axis is not None and axis < 0:
+            new_axis = axis + self._ndim
+            if self._ndim == 1 and new_axis != 0:
+                raise numpy.AxisError(axis, 1)
+            elif self._ndim == 2 and new_axis not in [0, 1]:
+                raise numpy.AxisError(axis, 2)
+            axis = new_axis
+        if self._ndim == 1:
+            if axis == 1:
+                raise numpy.AxisError(1, 1)
+            if self._query_compiler.isna().any(axis=1).any(axis=0).to_numpy()[0, 0]:
+                na_row_map = self._query_compiler.isna().any(axis=1)
+                result = na_row_map.idxmax().to_numpy()[0, 0]
+            else:
+                result = self._query_compiler.idxmin(axis=0)
+            if keepdims:
+                if out is not None and out.shape != (1,):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 0, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                return fix_dtypes_and_determine_return(
+                    result, 1, numpy.int64, out, True
+                )
+            return result.to_numpy()[0, 0]
+        if axis is None:
+            if self._query_compiler.isna().any(axis=1).any(axis=0).to_numpy()[0, 0]:
+                na_row_map = self._query_compiler.isna().any(axis=1)
+                na_row = self._query_compiler.getitem_array(na_row_map)
+                col_idx = na_row.to_numpy().argmax()
+                final_idxmax = na_row_map.idxmax().to_numpy().flatten()
+            else:
+                inner_idxs = self._query_compiler.idxmin(axis=1)
+                final_idxmax = (
+                    self._query_compiler.min(axis=1).idxmin(axis=0).to_numpy().flatten()
+                )
+                col_idx = inner_idxs.take_2d_positional(final_idxmax, [0]).to_numpy()[
+                    0, 0
+                ]
+            result = (self.shape[1] * final_idxmax[0]) + col_idx
+            if keepdims:
+                if out is not None and out.shape != (1, 1):
+                    raise ValueError(
+                        f"operand was set up as a reduction along axis 0, but the length of the axis is {out.shape[0]} (it has to be 1)"
+                    )
+                return fix_dtypes_and_determine_return(
+                    array(numpy.array([[result]], dtype=bool))._query_compiler,
+                    2,
+                    numpy.int64,
+                    out,
+                    True,
+                )
+            return result
+        if axis > 1:
+            raise numpy.AxisError(axis, 2)
+        result = self._query_compiler.idxmin(axis=axis)
+        na_mask = self._query_compiler.isna().any(axis=axis)
+        if na_mask.any(axis=axis ^ 1).to_numpy()[0, 0]:
+            na_idxs = self._query_compiler.isna().idxmax(axis=axis)
+            result = na_mask.where(na_idxs, result)
+        new_ndim = self._ndim - 1 if not keepdims else self._ndim
+        if new_ndim == 0:
+            result = result.to_numpy()[0, 0]
+            return result
+        if not keepdims and axis != 1:
+            result = result.transpose()
+        return fix_dtypes_and_determine_return(result, new_ndim, numpy.int64, out, True)
+
     def _isfinite(
         self,
         /,
diff --git a/modin/numpy/math.py b/modin/numpy/math.py
index d634e5dd198..69020360268 100644
--- a/modin/numpy/math.py
+++ b/modin/numpy/math.py
@@ -333,6 +333,16 @@ def mean(x1, axis=None, dtype=None, out=None, keepdims=None, *, where=True):
     return x1.mean(axis=axis, out=out, keepdims=keepdims, where=where, dtype=dtype)
 
 
+def var(x1, axis=None, dtype=None, out=None, keepdims=None, *, where=True):
+    x1 = try_convert_from_interoperable_type(x1)
+    if not isinstance(x1, array):
+        ErrorMessage.bad_type_for_numpy_op("var", type(x1))
+        return numpy.var(
+            x1, axis=axis, out=out, keepdims=keepdims, where=where, dtype=dtype
+        )
+    return x1.var(axis=axis, out=out, keepdims=keepdims, where=where, dtype=dtype)
+
+
 # Maximum and minimum are ufunc's in NumPy, which means that our array's __array_ufunc__
 # implementation will automatically handle this. We still need the function though, so that
 # if the operands are modin.pandas objects, we can convert them to arrays, but after that
@@ -397,3 +407,39 @@ def amin(x1, axis=None, out=None, keepdims=None, initial=None, where=True):
 
 
 min = amin
+
+
+def sqrt(
+    x, out=None, *, where=True, casting="same_kind", order="K", dtype=None, subok=True
+):
+    x = try_convert_from_interoperable_type(x)
+    if not isinstance(x, array):
+        ErrorMessage.bad_type_for_numpy_op("sqrt", type(x))
+        return numpy.sqrt(x, out, where, casting, order, dtype, subok)
+    return x.power(0.5, out, where, casting, order, dtype, subok)
+
+
+def exp(
+    x, out=None, *, where=True, casting="same_kind", order="K", dtype=None, subok=True
+):
+    x = try_convert_from_interoperable_type(x)
+    if not isinstance(x, array):
+        ErrorMessage.bad_type_for_numpy_op("exp", type(x))
+        return numpy.exp(x, out, where, casting, order, dtype, subok)
+    return x.exp(out, where, casting, order, dtype, subok)
+
+
+def argmax(a, axis=None, out=None, *, keepdims=None):
+    a = try_convert_from_interoperable_type(a)
+    if not isinstance(a, array):
+        ErrorMessage.bad_type_for_numpy_op("argmax", type(a))
+        return numpy.argmax(a, axis=axis, out=out, keepdims=keepdims)
+    return a.argmax(axis=axis, out=out, keepdims=keepdims)
+
+
+def argmin(a, axis=None, out=None, *, keepdims=None):
+    a = try_convert_from_interoperable_type(a)
+    if not isinstance(a, array):
+        ErrorMessage.bad_type_for_numpy_op("argmin", type(a))
+        return numpy.argmin(a, axis=axis, out=out, keepdims=keepdims)
+    return a.argmin(axis=axis, out=out, keepdims=keepdims)
diff --git a/modin/numpy/trigonometry.py b/modin/numpy/trigonometry.py
new file mode 100644
index 00000000000..a7f677b582f
--- /dev/null
+++ b/modin/numpy/trigonometry.py
@@ -0,0 +1,38 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team licenses this file to you under the
+# Apache License, Version 2.0 (the "License"); you may not use this file except in
+# compliance with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+import numpy
+
+from .arr import array
+from .utils import try_convert_from_interoperable_type
+from modin.error_message import ErrorMessage
+
+
+def tanh(
+    x, out=None, where=True, casting="same_kind", order="K", dtype=None, subok=True
+):
+    x = try_convert_from_interoperable_type(x)
+    if not isinstance(x, array):
+        ErrorMessage.bad_type_for_numpy_op("tanh", type(x))
+        return numpy.tanh(
+            x,
+            out=out,
+            where=where,
+            casting=casting,
+            order=order,
+            dtype=dtype,
+            subok=subok,
+        )
+    return x.tanh(
+        out=out, where=where, casting=casting, order=order, dtype=dtype, subok=subok
+    )


Code review comments:
diff_hunk: @@ -103,6 +112,18 @@ def where(condition, x=None, y=None):
     )
 
 
+def split(arr, indices, axis=0):
path: modin/numpy/__init__.py
text: @user1:
Prefer to move this to reshape.py