def do_substitution(target): # Check if target has a docstring that can be substituted to if target.__doc__: # Make a copy of the target docstring to check formatting later doc_presub = str(target.__doc__) # Try to use %-formatting try: target.__doc__ = target.__doc__ % (params) # If that raises an error, use .format with *args except TypeError: target.__doc__ = target.__doc__.format(*params) # Using **kwargs with % raises no errors if .format is required else: # Check if formatting was done and use .format if not if (target.__doc__ == doc_presub): target.__doc__ = target.__doc__.format(**params) # Raise error if target has no docstring else: raise InputError("Target has no docstring available for " "substitutions!") # Return the target definition return (target)
def get_outer_frame(func): """ Checks whether or not the calling function contains an outer frame corresponding to `func` and returns it if so. If this frame cannot be found, returns *None* instead. Parameters ---------- func : function The function or method whose frame must be located in the outer frames. Returns ------- outer_frame : frame or None The requested outer frame if it was found, or *None* if it was not. """ # If func is a function, obtain its name and module name if isfunction(func): name = func.__name__ module_name = func.__module__ # Else, if func is a method, obtain its name and class object elif ismethod(func): name = func.__name__ class_obj = func.__self__.__class__ # Else, raise error else: raise InputError("Input argument 'func' must be a callable function or" " method!") # Obtain the caller's frame caller_frame = currentframe().f_back # Loop over all outer frames for frame_info in getouterframes(caller_frame): # Check if frame has the correct name if (frame_info.function == name): # If func is a function, return if module name is also correct if (isfunction(func) and frame_info.frame.f_globals['__name__'] == module_name): return (frame_info.frame) # Else, return frame if class is also correct elif (frame_info.frame.f_locals['self'].__class__ is class_obj): return (frame_info.frame) else: return (None)
def docstring_substitute(*args, **kwargs): """ Custom decorator that allows either given positional arguments `args` or keyword arguments `kwargs` to be substituted into the docstring of the target function/class. Both `%` and `.format()` string formatting styles are supported. Keep in mind that this decorator will always attempt to do %-formatting first, and only uses `.format()` if the first fails. """ # Check if solely args or kwargs were provided if len(args) and len(kwargs): raise InputError("Either only positional or keyword arguments are " "allowed!") else: params = args or kwargs # This function performs the docstring substitution on a given definition def do_substitution(target): # Check if target has a docstring that can be substituted to if target.__doc__: # Make a copy of the target docstring to check formatting later doc_presub = str(target.__doc__) # Try to use %-formatting try: target.__doc__ = target.__doc__ % (params) # If that raises an error, use .format with *args except TypeError: target.__doc__ = target.__doc__.format(*params) # Using **kwargs with % raises no errors if .format is required else: # Check if formatting was done and use .format if not if (target.__doc__ == doc_presub): target.__doc__ = target.__doc__.format(**params) # Raise error if target has no docstring else: raise InputError("Target has no docstring available for " "substitutions!") # Return the target definition return (target) # Return decorator function return (do_substitution)
def check_instance(instance, cls): """ Checks if provided `instance` has been initialized from a proper `cls` (sub)class. Raises a :class:`~TypeError` if `instance` is not an instance of `cls`. Parameters ---------- instance : object Class instance that needs to be checked. cls : class The class which `instance` needs to be properly initialized from. Returns ------- result : bool Bool indicating whether or not the provided `instance` was initialized from a proper `cls` (sub)class. """ # Check if cls is a class if not isclass(cls): raise InputError("Input argument 'cls' must be a class!") # Check if instance was initialized from a cls (sub)class if not isinstance(instance, cls): raise TypeError("Input argument 'instance' must be an instance of the " "%s.%s class!" % (cls.__module__, cls.__name__)) # Retrieve a list of all cls attributes class_attrs = dir(cls) # Check if all cls attributes can be called in instance for attr in class_attrs: if not hasattr(instance, attr): return (False) else: return (True)
def sort2D(array, axis=-1, order=None): """ Sorts a 2D `array` over a given `axis` in the specified `order`. This function is different from NumPy's :func:`~numpy.sort` function in that it sorts over a given axis rather than along it, and the order can be given as integers rather than field strings. Parameters ---------- array : 2D array_like Input array that requires sorting. Optional -------- axis : int. Default: -1 Axis over which to sort the elements. Default is to sort all elements over the last axis. A negative value counts from the last to the first axis. order : int, 1D array_like of int or None. Default: None The order in which the vectors in the given `axis` need to be sorted. Negative values count from the last to the first vector. If *None*, all vectors in the given `axis` are sorted individually. Returns ------- array_sort : 2D :obj:`~numpy.ndarray` object Input `array` with its `axis` sorted in the specified `order`. Examples -------- Sorting the column elements of a given 2D array with no order specified: >>> array = np.array([[0, 5, 1], [7, 4, 9], [3, 13, 6], [0, 1, 8]]) >>> array array([[ 0, 5, 1], [ 7, 4, 9], [ 3, 13, 6], [ 0, 1, 8]]) >>> sort2D(array) array([[ 0, 1, 1], [ 0, 4, 6], [ 3, 5, 8], [ 7, 13, 9]]) Sorting the same array in only the first column: >>> sort2D(array, order=0) array([[ 0, 5, 1], [ 0, 1, 8], [ 3, 13, 6], [ 7, 4, 9]]) Sorting all three columns in order: >>> sort2D(array, order=(0, 1, 2)) array([[ 0, 1, 8], [ 0, 5, 1], [ 3, 13, 6], [ 7, 4, 9]]) Sorting all three columns in a different order: >>> sort2D(array, order=(0, 2, 1)) array([[ 0, 5, 1], [ 0, 1, 8], [ 3, 13, 6], [ 7, 4, 9]]) """ # Make sure that input array is a numpy array array = np.array(array) # Check if array is indeed 2D if (array.ndim != 2): raise ShapeError("Input argument 'array' must be two-dimensional!") else: # Obtain the number of vectors along the given axis try: n_vec = array.shape[axis] except Exception as error: raise InputError("Input argument 'axis' is invalid (%s)!" % (error)) # Move the given axis to be the first axis array = np.moveaxis(array, axis, 0) # If order is given, transform it into an array if order is not None: order = np.array(order, ndmin=1) # Check what order is given and act accordingly if order is None: array.sort(axis=-1) elif not (((-n_vec <= order) * (order < n_vec)).all()): raise ValueError("Input argument 'order' contains values that are " "out of bounds!") else: for i in reversed(order): array = array[:, np.argsort(array[i], kind='mergesort')] # Return the resulting array back after transforming its axes back return (np.moveaxis(array, 0, axis))
def rot90(array, axes=(0, 1), rot_axis='center', n_rot=1): """ Rotates the given `array` by 90 degrees around the point `rot_axis` in the given `axes`. This function is different from NumPy's :func:`~numpy.rot90` function in that every column (2nd axis) defines a different dimension instead of every individual axis. Parameters ---------- array : 2D array_like Array with shape [`n_pts`, `n_dim`] with `n_pts` the number of points and `n_dim` the number of dimensions. Requires: `n_dim` > 1. Optional -------- axes : 1D array_like with 2 ints. Default: (0, 1) Array containing the axes defining the rotation plane. Rotation is from the first axis towards the second. Can be omitted if `rot_axis` has length `n_dim`. rot_axis : 1D array_like of length 2/`n_dim` or 'center'. Default: 'center' If 'center', the rotation axis is chosen in the center of the minimum and maximum values found in the given `axes`. If 1D array of length 2, the rotation axis is chosen around the given values in the given `axes`. If 1D array of length `n_dim`, the rotation axis is chosen around the first two non-zero values. n_rot : int. Default: 1 Number of times to rotate `array` by 90 degrees. Returns ------- array_rot : 2D :obj:`~numpy.ndarray` object Array with shape [`n_pts`, `n_dim`] that has been rotated by 90 degrees `n_rot` times. Examples -------- Using an array with just two dimensions: >>> array = np.array([[0.75, 0], [0.25, 1], [1, 0.75], [0, 0.25]]) >>> rot90(array) array([[ 1. , 0.75], [ 0. , 0.25], [ 0.25, 1. ], [ 0.75, 0. ]]) Using the same array, but rotating it around a different point: >>> array = np.array([[0.75, 0], [0.25, 1], [1, 0.75], [0, 0.25]]) >>> rot90(array, rot_axis=[0.2, 0.7]) array([[ 0.9 , 1.25], [-0.1 , 0.75], [ 0.15, 1.5 ], [ 0.65, 0.5 ]]) """ # Make sure that array is a numpy array array = np.asarray(array) # Check if array is indeed two-dimensional and obtain the lengths if (array.ndim != 2): raise ShapeError("Input argument 'array' must be two-dimensional!") else: n_pts, n_dim = array.shape # Check axes axes = np.asarray(axes) if (axes.ndim == 1 and axes.shape[0] == 2 and (axes < n_dim).all()): pass else: raise InputError("Input argument 'axes' has invalid shape or values!") # Check what rot_axis is and act accordingly if (rot_axis == 'center'): rot_axis = np.zeros(2) rot_axis[0] =\ abs(np.max(array[:, axes[0]])+np.min(array[:, axes[0]]))/2 rot_axis[1] =\ abs(np.max(array[:, axes[1]])+np.min(array[:, axes[1]]))/2 elif (isinstance(rot_axis, str)): raise ValueError("Input argument 'rot_axis' can only have 'center' as" " a string value!") else: rot_axis = np.asarray(rot_axis) if (rot_axis.ndim == 1 and rot_axis.shape[0] == 2): pass elif (rot_axis.ndim == 1 and rot_axis.shape[0] == n_dim): axes = [] for i in range(n_dim): if (rot_axis[i] != 0): axes.append(i) if (len(axes) == 2): break else: raise ValueError("Input argument 'rot_axis' does not have two " "non-zero values!") rot_axis = rot_axis[axes] else: raise ShapeError("Input argument 'rot_axis' has invalid shape!") # Calculate the rotated matrix array_rot = array.copy() if (n_rot % 4 == 0): return (array_rot) elif (n_rot % 4 == 1): array_rot[:, axes[0]] = rot_axis[0] + rot_axis[1] - array[:, axes[1]] array_rot[:, axes[1]] = rot_axis[1] - rot_axis[0] + array[:, axes[0]] elif (n_rot % 4 == 2): array_rot[:, axes[0]] = 2 * rot_axis[0] - array[:, axes[0]] array_rot[:, axes[1]] = 2 * rot_axis[1] - array[:, axes[1]] elif (n_rot % 4 == 3): array_rot[:, axes[0]] = rot_axis[0] - rot_axis[1] + array[:, axes[1]] array_rot[:, axes[1]] = rot_axis[1] + rot_axis[0] - array[:, axes[0]] else: raise InputError("Input argument 'n_rot' is invalid!") # Return it return (array_rot)
def isin(array1, array2, axis=0, assume_unique=False, invert=False): """ Checks over the provided `axis` which elements of given `array1` are also in given `array2` and returns it. This is an nD-version of NumPy's :func:`~numpy.isin` function. Parameters ---------- array1 : array_like Input array. array2 : array_like Comparison array with same shape as `array1` except in given `axis`. Optional -------- axis : int or None. Default: 0 Axis over which elements must be checked in both arrays. A negative value counts from the last to the first axis. If *None*, both arrays are compared element-wise (this is the functionality of :func:`~numpy.isin`). assume_unique : bool. Default: False Whether to assume that the elements in both arrays are unique, which can speed up the calculation. invert : bool. Default: False Whether to invert the returned boolean values. If *True*, the values in `bool_array` are as if calculating ``array1 not in array2``. Returns ------- bool_array : :obj:`~numpy.ndarray` object of bool Bool array containing the elements found in `array1` that are in `array2` over given `axis`. Example ------- >>> array1 = np.array([[1, 2], [1, 3], [2, 1]]) >>> array2 = np.array([[1, 2], [1, 3]]) >>> isin(array1, array2) array([True, True, False]) """ # Check if axis is None if axis is None: # If so, use NumPy's isin function return (np.isin(array1, array2, assume_unique, invert)) # Make sure that given arrays are NumPy arrays array1 = np.asarray(array1) array2 = np.asarray(array2) # Make sure that 'axis' is the first axis of both arrays try: array1 = np.moveaxis(array1, axis, 0) array2 = np.moveaxis(array2, axis, 0) except Exception as error: raise InputError("Input argument 'axis' is invalid (%s)!" % (error)) # Convert arrays to lists list1 = array1.tolist() list2 = array2.tolist() # Determine what values in list1 are in list2 bool_list = [element in list2 for element in list1] # Convert bool_list to bool_array bool_array = np.array(bool_list, dtype=bool) # Return it return (~bool_array if invert else bool_array)
def diff(array1, array2=None, axis=0, flatten=True): """ Calculates the pair-wise differences between inputs `array1` and `array2` over the given `axis`. Parameters ---------- array1 : array_like One of the inputs used to calculate the pair-wise differences. Optional -------- array2 : array_like or None. Default: None The other input used to calculate the pair-wise differences. If *None*, `array2` is equal to `array1`. If not *None*, the length of all axes except `axis` must be equal for both arrays. axis : int. Default: 0 Over which axis to calculate the pair-wise differences. Default is over the first axis. A negative value counts from the last to the first axis. flatten : bool. Default: True If `array2` is *None*, whether or not to calculate all pair-wise differences. If *True*, a flattened array containing all above-diagonal pair-wise differences is returned. This is useful if only off-diagonal terms are required and the sign is not important. If *False*, an array with all pair-wise differences is returned. Returns ------- diff_array : :obj:`~numpy.ndarray` object Depending on the input parameters, an array with n dimensions containing the pair-wise differences between `array1` and `array2` over the given `axis`. Examples -------- Using two matrices returns the pair-wise differences in row-vectors: >>> mat1 = np.array([[1, 2, 3], [4, 5, 6]]) >>> mat2 = np.array([[4, 5, 6], [7, 8, 9]]) >>> diff(mat1, mat2) array([[[-3., -3., -3.], [-6., -6., -6.]], <BLANKLINE> [[ 0., 0., 0.], [-3., -3., -3.]]]) Setting `axis` to 1 returns the pair-wise differences in column-vectors: >>> mat1 = np.array([[1, 2, 3], [4, 5, 6]]) >>> mat2 = np.array([[4, 5, 6], [7, 8, 9]]) >>> diff(mat1, mat2, axis=1) array([[[-3., -3.], [-4., -4.], [-5., -5.]], <BLANKLINE> [[-2., -2.], [-3., -3.], [-4., -4.]], <BLANKLINE> [[-1., -1.], [-2., -2.], [-3., -3.]]]) Only using a single matrix returns the pair-wise differences in row-vectors in that matrix (either flattened or not): >>> mat = np.array([[1, 2, 3], [4, 5, 6]]) >>> diff(mat, flatten=True) array([[-3., -3., -3.]]) >>> diff(mat, flatten=False) array([[[ 0., 0., 0.], [-3., -3., -3.]], <BLANKLINE> [[ 3., 3., 3.], [ 0., 0., 0.]]]) Using a matrix and a vector returns the pair-wise differences in row-vectors: >>> mat = np.array([[1, 2, 3], [4, 5, 6]]) >>> vec = np.array([7, 8, 9]) >>> diff(mat, vec) array([[-6, -6, -6], [-3, -3, -3]]) Using two vectors returns the pair-wise differences in scalars: >>> vec1 = np.array([1, 2, 3]) >>> vec2 = np.array([4, 5, 6]) >>> diff(vec1, vec2) array([[-3., -4., -5.], [-2., -3., -4.], [-1., -2., -3.]]) """ # If array2 is not provided, both arrays are the same if array2 is None: # Make sure that input is a numpy array array1 = np.asarray(array1) # Check if a scalar has been provided and act accordingly if (array1.ndim == 0): return (0) # Swap axes in array to put the given axis as the first axis try: array1 = np.moveaxis(array1, axis, 0) except Exception as error: raise InputError("Input argument 'axis' is invalid (%s)!" % (error)) else: # Obtain the dimensionality and axis-length n_dim = array1.ndim len_axis = array1.shape[0] # If only unique pair-wise differences are requested if flatten: # Obtain the shape of the resulting array and initialize it n_diff = len_axis * (len_axis - 1) // 2 if (n_dim == 1): diff_shape = [n_diff] else: diff_shape = np.concatenate([[n_diff], array1.shape[1:n_dim]]) diff_array = np.zeros(diff_shape) # Initialize empty variable holding the distance in index of last i dist = 0 # Fill array for i in range(len_axis): diff_array[dist:dist + len_axis - i - 1] = array1[i] - array1[i + 1:] dist += len_axis - i - 1 # Return it return (diff_array) # If all difference are requested else: # Obtain the shape of the resulting array and initialize it diff_shape = np.concatenate([[len_axis], array1.shape]) diff_array = np.zeros(diff_shape) # Fill array for i in range(len_axis): diff_array[i] = array1[i] - array1 # Return it return (diff_array) # If array2 is provided, both arrays are different else: # Make sure that inputs are numpy arrays array1 = np.asarray(array1) array2 = np.asarray(array2) # Get number of dimensions n_dim1 = array1.ndim n_dim2 = array2.ndim # Check if both arrays are scalars and act accordingly if (n_dim1 == n_dim2 == 0): return (array1 - array2) # If both arrays have the same number of dimensions if (n_dim1 == n_dim2): # Swap axes in arrays to put the given axis as the first axis try: array1 = np.moveaxis(array1, axis, 0) array2 = np.moveaxis(array2, axis, 0) except Exception as error: raise InputError("Input argument 'axis' is invalid (%s)!" % (error)) else: # Obtain axis-length len_axis1 = array1.shape[0] # Check if the length of all other axes are the same if (array1.shape[1:] != array2.shape[1:]): raise ShapeError("Input arguments 'array1' and 'array2' do not" " have the same axes lengths: %s != %s" % (array1.shape[1:], array2.shape[1:])) # Obtain the shape of the resulting array and initialize it diff_shape = np.concatenate([[len_axis1], array2.shape]) diff_array = np.zeros(diff_shape) # Fill array for i in range(len_axis1): diff_array[i] = array1[i] - array2 # Return it return (diff_array) # If the arrays have different number of dimensions else: # If second array is bigger than first, swap them if (n_dim1 < n_dim2): # Swap arrays temp_array = array1 array1 = array2 array2 = temp_array # Swap ndims temp_ndim = n_dim1 n_dim1 = n_dim2 n_dim2 = temp_ndim # Save that arrays were swapped sign = -1 else: sign = 1 # Swap axes in the bigger array to put the given axis as first axis try: array1 = np.moveaxis(array1, axis, 0) except Exception as error: raise InputError("Input argument 'axis' is invalid (%s)!" % (error)) # Check if the length of all other axes are the same if (array1.shape[1:n_dim1] != array2.shape): args = ((array1.shape[1:n_dim1], array2.shape) if (sign == 1) else (array2.shape, array1.shape[1:n_dim1])) raise ShapeError("Input arguments 'array1' and 'array2' do" " not have the same axes lengths: %s != " "%s" % args) else: # Return difference array return (sign * (array1 - array2))
def draw_textline(text, *, x=None, y=None, pos='start top', ax=None, line_kwargs={}, text_kwargs={}): """ Draws a line on the axis `ax` in a :obj:`~matplotlib.figure.Figure` instance and prints `text` on top. Parameters ---------- text : str Text to be printed on the line. x : scalar or None If scalar, text/line x-coordinate. If *None*, line covers complete x-axis. Either `x` or `y` needs to be *None*. y : scalar or None If scalar, text/line y-coordinate. If *None*, line covers complete y-axis. Either `x` or `y` needs to be *None*. Optional -------- pos : {'start', 'end'}{'top', 'bottom'}. Default: 'start top' If 'start', prints the text at the start of the drawn line. If 'end', prints the text at the end of the drawn line. If 'top', prints the text above the drawn line. If 'bottom', prints the text below the drawn line. Arguments must be given as a single string. ax : :obj:`~matplotlib.axes.Axes` object or None. Default: None If :obj:`~matplotlib.axes.Axes` object, draws line in specified :obj:`~matplotlib.figure.Figure` instance. If *None*, draws line in current :obj:`~matplotlib.figure.Figure` instance. line_kwargs : dict of :class:`~matplotlib.lines.Line2D` properties.\ Default: {} The keyword arguments used for drawing the line. text_kwargs : dict of :class:`~matplotlib.text.Text` properties.\ Default: {} The keyword arguments used for drawing the text. """ # If no AxesSubplot object is provided, make one if ax is None: ax = plt.gca() # Convert pos to lowercase pos = pos.lower() # Set default line_kwargs and text_kwargs default_line_kwargs = {'linestyle': '-', 'color': 'k'} default_text_kwargs = {'color': 'k', 'fontsize': 14} # Combine given kwargs with default ones default_line_kwargs.update(line_kwargs) default_text_kwargs.update(text_kwargs) line_kwargs = default_line_kwargs text_kwargs = default_text_kwargs # Check if certain keyword arguments are present in text_fmt text_keys = list(text_kwargs.keys()) for key in text_keys: if key in ('va', 'ha', 'verticalalignment', 'horizontalalignment', 'rotation', 'transform', 'x', 'y', 's'): text_kwargs.pop(key) # Set line specific variables if x is None and y is not None: ax.axhline(y, **line_kwargs) elif x is not None and y is None: ax.axvline(x, **line_kwargs) else: raise InputError("Either of input arguments 'x' and 'y' needs to be " "*None*!") # Gather case specific text properties if ('start') in pos and ('top') in pos: ha = 'left' if x is None else 'right' va = 'bottom' other_axis = 0 elif ('start') in pos and ('bottom') in pos: ha = 'left' va = 'top' if x is None else 'bottom' other_axis = 0 elif ('end') in pos and ('top') in pos: ha = 'right' va = 'bottom' if x is None else 'top' other_axis = 1 elif ('end') in pos and ('bottom') in pos: ha = 'right' if x is None else 'left' va = 'top' other_axis = 1 else: raise ValueError("Input argument 'pos' is invalid!") # Set proper axes and rotation if x is None: x = other_axis rotation = 0 transform = transforms.blended_transform_factory( ax.transAxes, ax.transData) else: y = other_axis rotation = 90 transform = transforms.blended_transform_factory( ax.transData, ax.transAxes) # Draw text ax.text(x, y, text, rotation=rotation, ha=ha, va=va, transform=transform, **text_kwargs)