# # SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed 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. # """ This file includes utility functions for arrays/tensors that work for multiple libraries like NumPy and PyTorch. """ import builtins import functools import math import numbers from polygraphy import mod from polygraphy.datatype import DataType from polygraphy.logger import G_LOGGER np = mod.lazy_import("numpy") torch = mod.lazy_import("torch>=1.13.0") @mod.export() def is_torch(obj): """ Whether the provided object is a PyTorch tensor. This function does *not* introduce a dependency on the PyTorch module. Args: obj (Any): The object to check. Returns: bool: Whether the object is a PyTorch tensor. """ return ( torch.is_installed() and torch.is_importable() and isinstance(obj, torch.Tensor) ) @mod.export() def is_numpy(obj): """ Whether the provided object is a NumPy array or scalar. This function does *not* introduce a dependency on the NumPy module. Args: obj (Any): The object to check. Returns: bool: Whether the object is a NumPy array. """ return ( np.is_installed() and np.is_importable() and (isinstance(obj, np.ndarray) or isinstance(obj, np.generic)) ) @mod.export() def is_device_view(obj): """ Whether the provided object is a DeviceView array. Args: obj (Any): The object to check. Returns: bool: Whether the object is a DeviceView. """ from polygraphy.cuda import DeviceView return isinstance(obj, DeviceView) # The current design dispatches to the correct function implementation separately for each function call. # Obviously, this has some performance cost and an alternative approach would be a more familiar inheritance # pattern wherein we would have a BaseArray class and then child classes like NumpyArray, TorchArray, PolygraphyDeviceArray etc. # That way, the dispatching logic would only have to run once when we construct an instance of one of these # classes. # # The tradeoff is that the caller would then have to be careful that they are *not* passing in NumPy arrays, # Torch tensors etc. directly, but have first wrapped them appropriately. Plus, at the interface boundaries, # we would have to unwrap them once again since we don't want to expose the wrappers at the API level (the user # should be able to work directly with NumPy arrays, PyTorch tensors etc.). # # To illustrate this a bit better, consider the two possible workflows: # # Option 1 (dispatch logic in each function, current design): # # def my_api_func(obj) # nbytes = util.array.nbytes(obj) # Dispatching logic needs to run on each function call # dtype = util.array.dtype(obj) # # Do something interesting, then... # return obj # # Option 2 (class hierarchy, possible alternative design): # # # Assume we have: # # class BaseArray: # ... # # class TorchArray: # ... # # # etc. # # def my_api_func() # obj = wrap_array(obj) # Dispatch logic only runs once # nbytes = obj.nbytes # dtype = obj.dtype # # Do something interesting, then... # return unwrap_array(obj) # Need to return the np.ndarray/torch.Tensor/DeviceView, *not* the wrapper # # In Polygraphy, the number of calls to `wrap_array`/`unwrap_array` would most likely be quite high # relative to the number of calls to the actual methods, so the perfomance hit of the current implementation # may not be that significant. If it is, then it should be straightforward, though time-consuming, to switch to Option 2. # def dispatch(num_arrays=1): """ Decorator that will dispatch to functions specific to a framework type, like NumPy or PyTorch, based on the type of the input. The decorated function should return a dictionary with implementations for all supported types. The following keys may be specified: ["torch", "numpy", "device_view", "number"]. Args: num_arrays (int): The number of arrays expected. The naming convention for the array arguments is as follows: - For a single array, the argument is called "obj". - For two arrays, the arguments are called "lhs" and "rhs". - For N>2 arrays, the arguments are called "obj0", "obj1", ... "obj" In the case of more than one array, this function will automatically convert the rest to be of the same kind as the first. """ def dispatch_impl(func): def _get_key(obj): key = None if is_device_view(obj): key = "device_view" elif is_numpy(obj): key = "numpy" elif is_torch(obj): key = "torch" elif isinstance(obj, numbers.Number): key = "number" if not key: G_LOGGER.critical( f"Function: {func.__name__} is unsupported for objects of type: {type(obj).__name__}" ) return key if num_arrays < 0: G_LOGGER.critical( f"Function: {func.__name__} is unsupported with {num_arrays} < 0" ) @functools.wraps(func) def wrapped(*args, **kwargs): if len(args) < num_arrays: G_LOGGER.critical( f"Function: {func.__name__} is unsupported for less than {num_arrays} positional arguments" ) mapping = func() obj0 = args[0] key = _get_key(obj0) if key not in mapping: G_LOGGER.critical( f"Function: {func.__name__} is unsupported for objects of type: {type(obj0).__name__}" ) # Note that we can use to_torch/to_numpy here without a circular dependency because those functions # take the num_arrays=1 path. def convert_array(obj): if key == "torch": return to_torch(obj) elif key == "numpy": return to_numpy(obj) else: G_LOGGER.critical( f"Function: {func.__name__} is unsupported for objects of type: {type(obj).__name__}" ) converted_args = ( [obj0] + list(map(convert_array, args[1:num_arrays])) + list(args[num_arrays:]) ) return mapping[key](*converted_args, **kwargs) return wrapped return dispatch_impl ## ## Conversion Functions ## @mod.export() @dispatch() def to_torch(): """ Converts an array or tensor to a PyTorch tensor. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: torch.Tensor: The PyTorch tensor. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj, "numpy": lambda obj: torch.from_numpy(obj), "number": lambda obj: torch.tensor(obj), } @mod.export() @dispatch() def to_numpy(): """ Converts an array or tensor to a NumPy array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: np.ndarray: The NumPy array. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.numpy(force=True), "numpy": lambda obj: obj, "number": lambda obj: np.array(obj), } ## ## Metadata ## @mod.export() @dispatch() def nbytes(): """ Calculate the number of bytes required by the input array. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: int: The number of bytes required by the array. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.nelement() * obj.element_size(), "numpy": lambda obj: obj.nbytes, "device_view": lambda obj: obj.nbytes, } @mod.export() @dispatch() def size(): """ Calculate the volume of the input array Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: int: The volume of the array. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.numel(), "numpy": lambda obj: obj.size, } @mod.export() @dispatch() def data_ptr(): """ Return a pointer to the first element of the input array. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: int: A pointer to the first element of the array. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.data_ptr(), "numpy": lambda obj: obj.ctypes.data, "device_view": lambda obj: obj.ptr, } @mod.export() @dispatch() def is_on_cpu(): """ Returns whether the input array is in CPU memory. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: bool: Whether the array is in CPU, i.e. host, memory. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.device.type == "cpu", "numpy": lambda _: True, "device_view": lambda _: False, } @mod.export() @dispatch() def is_on_gpu(): """ Returns whether the input array is in GPU memory. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: bool: Whether the array is in GPU, i.e. host, memory. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.device.type == "cuda", "numpy": lambda _: False, "device_view": lambda _: True, } @mod.export() @dispatch() def dtype(): """ Return the data type the input array. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: DataType: The data type of the array Raises: PolygraphyException: if the input is of an unrecognized type. """ func = lambda obj: DataType.from_dtype(obj.dtype) return {"torch": func, "numpy": func, "device_view": func} @mod.export() @dispatch() def shape(): """ Return the shape the input array. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray, DeviceView]: The shape of the array Raises: PolygraphyException: if the input is of an unrecognized type. """ func = lambda obj: obj.shape return {"torch": func, "numpy": func, "device_view": func} @mod.export() def view(obj, dtype, shape): """ Return a view of the the input array with the given data type and shape. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Must be contiguous. dtype (DataType): The data type to use for the view. shape (Sequence[int]): The shape to use for the view. Returns: Union[torch.Tensor, numpy.ndarray, DeviceView]: The view of the array Raises: PolygraphyException: if the input is of an unrecognized type. """ if not is_contiguous(obj): G_LOGGER.critical(f"Input array to view() must be contiguous in memory") if is_device_view(obj): return obj.view(shape=shape, dtype=dtype) dtype = ( DataType.to_dtype(dtype, "numpy") if is_numpy(obj) else DataType.to_dtype(dtype, "torch") ) return obj.reshape(-1).view(dtype).reshape(shape) @mod.export() @dispatch() def is_contiguous(): """ Checks whether the provided array is contiguous in memory. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: bool: Whether the array is contiguous in memory. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda obj: obj.is_contiguous(), "numpy": lambda obj: obj.flags["C_CONTIGUOUS"], "device_view": lambda _: True, } ## ## Memory Management ## @mod.export() @dispatch() def make_contiguous(): """ Makes an array contiguous if it's not already. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceView]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray, DeviceView]: The contiguous array. Raises: PolygraphyException: if the input is of an unrecognized type. """ def impl_numpy(obj): if is_contiguous(obj): return obj return np.ascontiguousarray(obj) return { "torch": lambda obj: obj.contiguous(), "numpy": impl_numpy, "device_view": lambda obj: obj, } @mod.export() @dispatch() def resize_or_reallocate(): """ Resizes the provided buffer, possibly reallocating the buffer. Args: obj (Union[torch.Tensor, numpy.ndarray, DeviceArray]): The array or tensor. shape (Sequence[int]): The desired shape of the buffer. Returns: Union[torch.Tensor, numpy.ndarray, DeviceArray]: The resized buffer, possibly reallocated. """ def numpy_impl(obj, shape): if shape != obj.shape: try: obj.resize(shape, refcheck=False) except ValueError as err: G_LOGGER.warning( f"Could not resize NumPy array to shape: {shape}. " f"Allocating a new array instead.\nNote: Error was: {err}" ) obj = np.empty(shape, dtype=np.dtype(obj.dtype)) return obj return { "numpy": numpy_impl, "torch": lambda obj, shape: obj.resize_(shape) if shape != obj.shape else obj, "device_view": lambda obj, shape: ( obj.resize(shape) if shape != obj.shape else obj ), } ## ## Math Helpers ## @mod.export() @dispatch() def cast(): """ Casts an array to the specified type. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. dtype (DataType): The type to cast to. Returns: Union[torch.Tensor, numpy.ndarray]: The casted array. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj, dtype: np.array(obj.astype(dtype.numpy())), "torch": lambda obj, dtype: obj.to(DataType.to_dtype(dtype, "torch")), } @mod.export() @dispatch() def any(): """ Return whether any of the values in the provided array evaluate to True. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: bool: Whether any of the values in the array evaluate to True. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.any(obj), "torch": lambda obj: bool(torch.any(obj)), } @mod.export() @dispatch() def all(): """ Return whether all of the values in the provided array evaluate to True. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: bool: Whether all of the values in the array evaluate to True. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.all(obj), "torch": lambda obj: bool(torch.all(obj)), } @mod.export() @dispatch(num_arrays=2) def equal(): """ Returns whether two arrays are equal Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: bool: Whether the arrays are equal. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda lhs, rhs: torch.equal(lhs, rhs), "numpy": lambda lhs, rhs: np.array_equal(lhs, rhs), } @mod.export() @dispatch(num_arrays=2) def subtract(): """ Subtracts the second array from the first. Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The difference. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda lhs, rhs: lhs - rhs, "numpy": lambda lhs, rhs: np.array(lhs - rhs), } @mod.export() @dispatch(num_arrays=2) def divide(): """ Divides the first array by the second. Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The quotient. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "torch": lambda lhs, rhs: lhs / rhs, "numpy": lambda lhs, rhs: lhs / rhs, } @mod.export() @dispatch(num_arrays=2) def allclose(): """ Returns whether all the values in two arrays are within the given thresholds. Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. rtol (float): The relative tolerance. Defaults to 1e-5. atol (float): The absolute tolerance. Defaults to 1e-8. Returns: bool: Whether the arrays are close. Raises: PolygraphyException: if the input is of an unrecognized type. """ DEFAULT_RTOL = 1e-5 DEFAULT_ATOL = 1e-8 return { "torch": lambda lhs, rhs, rtol=DEFAULT_RTOL, atol=DEFAULT_ATOL: torch.allclose( lhs, rhs, rtol=rtol, atol=atol ), "numpy": lambda lhs, rhs, rtol=DEFAULT_RTOL, atol=DEFAULT_ATOL: np.allclose( lhs, rhs, rtol=rtol, atol=atol ), } @mod.export() def unravel_index(index, shape): """ Unravels a flat index into a N-dimensional index based on the specified shape. Args: index (int): The flat index. shape (Sequence[int]): The shape on which to unravel the index. Returns: Tuple[int]: The N-dimensional index. Raises: PolygraphyException: if the input is of an unrecognized type. """ index = int(index) nd_index = [] for dim in reversed(shape): nd_index.insert(0, index % dim) index = index // dim return tuple(nd_index) @mod.export() @dispatch() def histogram(): """ Compute a histogram for the given array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. range (Tuple[float, float]): The lower and upper range of the bins. Returns: Tuple[Union[torch.Tensor, numpy.ndarray], Union[torch.Tensor, numpy.ndarray]]: The histogram values and the bin edges Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj, range=None): # PyTorch doesn't support histograms for all types, so cast to FP32 original_dtype = obj.dtype hist, bins = torch.histogram(obj.to(torch.float32), bins=10, range=range) return hist.to(original_dtype), bins.to(original_dtype) return { "numpy": lambda obj, range=None: np.histogram(obj, bins=10, range=range), "torch": torch_impl, } @mod.export() @dispatch() def max(): """ Returns the maximum value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The maximum value Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.amax(obj).item(), "torch": lambda obj: torch.max(obj).item(), } @mod.export() @dispatch() def argmax(): """ Returns the flattened index of the maximum value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: int: The flattened index. Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj): # Torch argmax doesn't support bools return torch.argmax(obj.to(torch.float32)) return { "numpy": lambda obj: np.argmax(obj), "torch": lambda obj: torch_impl(obj), } @mod.export() @dispatch() def min(): """ Returns the minimum value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The minimum value Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.amin(obj).item(), "torch": lambda obj: torch.min(obj).item(), } @mod.export() @dispatch() def argmin(): """ Returns the flattened index of the minimum value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: int: The flattened index. Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj): # Torch argmin doesn't support bools return torch.argmin(obj.to(torch.float32)) return { "numpy": lambda obj: np.argmin(obj), "torch": lambda obj: torch_impl(obj), } @mod.export() @dispatch() def mean(): """ Returns the mean value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. dtype (DataType): The mean compute type. Returns: Any: The mean value Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj, dtype=None: np.mean( obj, dtype=DataType.to_dtype(dtype, "numpy") if dtype is not None else None ), "torch": lambda obj, dtype=None: torch.mean( obj, dtype=DataType.to_dtype(dtype, "torch") if dtype is not None else None ), } @mod.export() @dispatch() def std(): """ Returns the standard deviation of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The standard deviation Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj): # torch.var is only supported for floats, so cast up and then back. obj_fp32 = obj.to(torch.float32) try: return torch.std(obj_fp32, correction=0) except AttributeError: return torch.std(obj_fp32, unbiased=False) return { "numpy": lambda obj: np.std(obj), "torch": torch_impl, } @mod.export() @dispatch() def var(): """ Returns the variance of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The variance Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj): # torch.var is only supported for floats, so cast up and then back. obj_fp32 = obj.to(torch.float32) try: return torch.var(obj_fp32, correction=0) except AttributeError: return torch.var(obj_fp32, unbiased=False) return { "numpy": lambda obj: np.var(obj), "torch": torch_impl, } @mod.export() @dispatch() def median(): """ Returns the median value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The median value Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_impl(obj): # Median in PyTorch doesn't work as expected for arrays with an even number of elements - instead # of returning the average of the two middle elements, it just returns the smaller one. # It is also not implemented for some types, so cast to FP32 for compute. original_dtype = obj.dtype obj = obj.to(torch.float32) rv = 0 if obj.nelement() % 2 == 1: rv = torch.median(obj) else: smaller = torch.median(obj) larger = torch.median(torch.cat([obj.flatten(), torch.max(obj)[None]])) rv = (smaller + larger) / 2.0 return rv.to(original_dtype) return { "numpy": lambda obj: np.median(obj), "torch": torch_impl, } @mod.export() @dispatch() def quantile(): """ Returns the value of the q quantile of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. q (float): Quantile to compute, expected range [0, 1] Returns: Any: The quantile value Raises: PolygraphyException: if the input is of an unrecognized type. """ def numpy_impl(obj, q): if obj.size == 0: return np.inf return np.quantile(obj, q) def torch_impl(obj, q): if obj.numel() == 0: return torch.inf original_dtype = obj.dtype obj = obj.to(torch.float32) qunatile_val = torch.quantile(obj, q) return qunatile_val.to(original_dtype) return { "numpy": numpy_impl, "torch": torch_impl, } @mod.export() @dispatch() def topk(): """ Returns a tuple of the top k values and indices of an array along a specified axis. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. k (int): The number of values to return. This is clamped to the length of obj along the given axis. axis (int): The axis to perform the topk computation on Returns: Tuple[Union[torch.Tensor, numpy.ndarray], Union[torch.Tensor, numpy.ndarray]]: A tuple containing a pair of arrays, the first being the values and the second being the indices of the top k values along the specified axis Raises: PolygraphyException: if the input is of an unrecognized type. """ def numpy_impl(obj, k, axis): # NumPy doesn't have a Top K implementation indices = np.argsort(-obj, axis=axis, kind="stable") axis_len = indices.shape[axis] indices = np.take(indices, np.arange(0, builtins.min(k, axis_len)), axis=axis) return np.take_along_axis(obj, indices, axis=axis), indices def torch_impl(obj, k, axis): axis_len = obj.shape[axis] # Top K has no implementation for float16 in torch-cpu, so # If gpu is available, run computation there # Otherwise, run the calculation on cpu using fp32 precision if obj.dtype == torch.float16: if torch.cuda.is_available(): original_device = obj.device ret = tuple( torch.topk(obj.to("cuda"), builtins.min(k, axis_len), dim=axis) ) return (ret[0].to(original_device), ret[1].to(original_device)) else: ret = tuple( torch.topk( obj.type(torch.float32), builtins.min(k, axis_len), dim=axis ) ) return (ret[0].type(torch.float16), ret[1].type(torch.float16)) return tuple(torch.topk(obj, builtins.min(k, axis_len), dim=axis)) return { "numpy": numpy_impl, "torch": torch_impl, } @mod.export() @dispatch() def abs(): """ Returns the absolute value of an array. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Any: The absolute value Raises: PolygraphyException: if the input is of an unrecognized type. """ def torch_abs_impl(obj): # PyTorch doesn't support abs for all types, so cast to FP32 original_dtype = obj.dtype return torch.abs(obj.to(torch.float32)).to(original_dtype) return { "numpy": lambda obj: np.array(np.abs(obj)), "torch": lambda obj: torch_abs_impl(obj), } @mod.export() @dispatch() def isfinite(): """ Returns a boolean array indicating if each element of obj is finite or not. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The boolean array indicating which elements of obj are finite. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.isfinite(obj), "torch": lambda obj: torch.isfinite(obj), } @mod.export() @dispatch() def isinf(): """ Returns a boolean array indicating if each element of obj is infinite or not. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The boolean array indicating which elements of obj are infinite. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.isinf(obj), "torch": lambda obj: torch.isinf(obj), } @mod.export() @dispatch() def isnan(): """ Returns a boolean array indicating if each element of obj is NaN or not. Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The boolean array indicating which elements of obj are NaN. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.isnan(obj), "torch": lambda obj: torch.isnan(obj), "number": lambda obj: math.isnan(obj), } @mod.export() @dispatch() def argwhere(): """ Returns a indices of non-zero array elements Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: An (N, obj.ndim) array containing indices of non-zero elements of obj Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.argwhere(obj), "torch": lambda obj: torch.argwhere(obj), } @mod.export() @dispatch() def ravel(): """ Flattens the input array Args: obj (Union[torch.Tensor, numpy.ndarray]): The array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The flattened input tensor Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.ravel(obj), "torch": lambda obj: torch.ravel(obj), } @mod.export() @dispatch() def logical_not(): """ Computes the logical not of an array Args: obj (Union[torch.Tensor, numpy.ndarray]): The input array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The logical not. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda obj: np.logical_not(obj), "torch": lambda obj: torch.logical_not(obj), } @mod.export() @dispatch(num_arrays=2) def logical_xor(): """ Computes the logical exclusive-or of two arrays. Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The logical xor. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda lhs, rhs: np.logical_xor(lhs, rhs), "torch": lambda lhs, rhs: torch.logical_xor(lhs, rhs), } @mod.export() @dispatch(num_arrays=2) def logical_and(): """ Computes the logical and of two arrays. Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: The logical and. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda lhs, rhs: np.logical_and(lhs, rhs), "torch": lambda lhs, rhs: torch.logical_and(lhs, rhs), } @mod.export() @dispatch(num_arrays=2) def greater(): """ Returns a boolean array indicating where lhs is greater than rhs Args: lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: Boolean array indicating whether lhs > rhs. Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda lhs, rhs: np.greater(lhs, rhs), "torch": lambda lhs, rhs: torch.gt(lhs, rhs), } @mod.export() @dispatch(num_arrays=3) def where(): """ Returns an array containing elements from lhs when cond is true, and rhs when cond is false. Computes the logical and of two arrays. Args: cond (Union[torch.Tensor, numpy.ndarray]): The condition array or tensor. lhs (Union[torch.Tensor, numpy.ndarray]): The first array or tensor. rhs (Union[torch.Tensor, numpy.ndarray]): The second array or tensor. Returns: Union[torch.Tensor, numpy.ndarray]: Selected elements from lhs if cond is true, and rhs otherwise Raises: PolygraphyException: if the input is of an unrecognized type. """ return { "numpy": lambda cond, lhs, rhs: np.where(cond, lhs, rhs), "torch": lambda cond, lhs, rhs: torch.where(cond, lhs, rhs), }