diff --git a/news/deprecate.rst b/news/deprecate.rst new file mode 100644 index 00000000..cb644fef --- /dev/null +++ b/news/deprecate.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* Old tests and source files from prior, pre-release development. + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/snmf/containers.py b/src/diffpy/snmf/containers.py deleted file mode 100644 index 1af960ad..00000000 --- a/src/diffpy/snmf/containers.py +++ /dev/null @@ -1,77 +0,0 @@ -import numdifftools -import numpy as np - - -class ComponentSignal: - """ - Attributes - ---------- - grid: 1d array of floats - The vector containing the grid points of the component. - iq: 1d array of floats - The intensity/g(r) values of the component. - weights: 1d array of floats - The vector containing the weight of the component signal for each signal. - stretching_factors: 1d array of floats - The vector containing the stretching factor for the component signal for each signal. - id: int - The number identifying the component. - """ - - def __init__(self, grid, number_of_signals, id_number, perturbation=1e-3): - self.grid = np.asarray(grid) - self.iq = np.random.rand(len(grid)) - self.weights = np.random.rand(number_of_signals) - self.stretching_factors = np.ones(number_of_signals) + np.random.randn(number_of_signals) * perturbation - self.id = int(id_number) - - def apply_stretch(self, m): - """Applies a stretching factor to a component - - Parameters - ---------- - m: int - The index specifying which stretching factor to apply - - Returns - ------- - tuple of 1d arrays - The tuple of vectors where one vector is the stretched component, one vector is the 1st derivative of the - stretching operation, and one vector is the second derivative of the stretching operation. - """ - normalized_grid = np.arange(len(self.grid)) - interpolate_intensity = lambda stretching_factor: np.interp( # noqa: E731 - normalized_grid / stretching_factor, normalized_grid, self.iq, left=0, right=0 - ) - derivative_func = numdifftools.Derivative(interpolate_intensity) - second_derivative_func = numdifftools.Derivative(derivative_func) - - stretched_component = interpolate_intensity(self.stretching_factors[m]) - stretched_component_gra = derivative_func(self.stretching_factors[m]) - stretched_component_hess = second_derivative_func(self.stretching_factors[m]) - - return ( - np.asarray(stretched_component), - np.asarray(stretched_component_gra), - np.asarray(stretched_component_hess), - ) - - def apply_weight(self, m, stretched_component=None): - """Applies as weight factor to a component signal. - - Parameters - ---------- - m: int - The index specifying with weight to apply - stretched_component: 1d array - The 1d array containing a stretched component. - - Returns - ------- - 1d array - The vector containing a component signal or stretched component signal with a weight factor applied. - """ - if stretched_component is None: - return self.iq * self.weights[m] - else: - return stretched_component * self.weights[m] diff --git a/src/diffpy/snmf/factorizers.py b/src/diffpy/snmf/factorizers.py deleted file mode 100644 index b4620fd4..00000000 --- a/src/diffpy/snmf/factorizers.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np -import scipy.optimize - - -def lsqnonneg(stretched_component_matrix, target_signal): - """Finds the weights of stretched component signals under one-sided constraint. - - Solves ``argmin_x || Ax - b ||_2`` for ``x>=0`` where A is the stretched_component_matrix and b is the - target_signal vector. Finds the weights of component signals given undecomposed signal data and stretched - components under a one-sided constraint on the weights. - - Parameters - ---------- - stretched_component_matrix: 2d array like - The component matrix where each column contains a stretched component signal. Has dimensions R x C where R is - the length of the signal and C is the number of components. Does not need to be nonnegative. Corresponds with - 'A' from the objective function. - - target_signal: 1d array like - The signal that is used as reference against which weight factors will be determined. Any column from the - matrix of the entire, unfactorized input data could be used. Has length R. Does not need to be nonnegative. - Corresponds with 'b' from the objective function. - - Returns - ------- - 1d array like - The vector containing component signal weights at a moment. Has length C. - """ - stretched_component_matrix = np.asarray(stretched_component_matrix) - target_signal = np.asarray(target_signal) - return scipy.optimize.nnls(stretched_component_matrix, target_signal)[0] diff --git a/src/diffpy/snmf/io.py b/src/diffpy/snmf/io.py deleted file mode 100644 index 12eb1b23..00000000 --- a/src/diffpy/snmf/io.py +++ /dev/null @@ -1,120 +0,0 @@ -from pathlib import Path - -import numpy as np -import scipy.sparse - -from diffpy.utils.parsers.loaddata import loadData - - -def initialize_variables(data_input, number_of_components, data_type, sparsity=1, smoothness=1e18): - """Determines the variables and initial values used in the SNMF algorithm. - - Parameters - ---------- - data_input: 2d array like - The observed or simulated PDF or XRD data provided by the user. Has dimensions R x N where R is the signa - length and N is the number of PDF/XRD signals. - - number_of_components: int - The number of component signals the user would like to decompose 'data_input' into. - - data_type: str - The type of data the user has passed into the program. Can assume the value of 'PDF' or 'XRD.' - - sparsity: float, optional - The regularization parameter that behaves as the coefficient of a "sparseness" regularization term that - enhances the ability to decompose signals in the case of sparse data e.g. X-ray Diffraction data. - A non-zero value indicates sparsity in the data; greater magnitudes indicate greater amounts of sparsity. - - smoothness: float, optional - The regularization parameter that behaves as the coefficient of a "smoothness" term that ensures that - component signal weightings change smoothly with time. Assumes a default value of 1e18. - - Returns - ------- - dictionary - The collection of the names and values of the constants used in the algorithm. Contains the number of - observed PDF/XRD patterns, the length of each pattern, the type of the data, the number of components - the user would like to decompose the data into, an initial guess for the component matrix, and initial - guess for the weight factor matrix, an initial guess for the stretching factor matrix, a parameter - controlling smoothness of the solution, a parameter controlling sparseness of the solution, the matrix - representing the smoothness term, and a matrix used to construct a hessian matrix. - - """ - signal_length = data_input.shape[0] - number_of_signals = data_input.shape[1] - - diagonals = [ - np.ones(number_of_signals - 2), - -2 * np.ones(number_of_signals - 2), - np.ones(number_of_signals - 2), - ] - smoothness_term = 0.25 * scipy.sparse.diags( - diagonals, [0, 1, 2], shape=(number_of_signals - 2, number_of_signals) - ) - - hessian_helper_matrix = scipy.sparse.block_diag([smoothness_term.T @ smoothness_term] * number_of_components) - sequence = ( - np.arange(number_of_signals * number_of_components) - .reshape(number_of_components, number_of_signals) - .T.flatten() - ) - hessian_helper_matrix = hessian_helper_matrix[sequence, :][:, sequence] - - return { - "signal_length": signal_length, - "number_of_signals": number_of_signals, - "number_of_components": number_of_components, - "data_type": data_type, - "smoothness": smoothness, - "sparsity": sparsity, - "smoothness_term": smoothness_term, - "hessian_helper_matrix": hessian_helper_matrix, - } - - -def load_input_signals(file_path=None): - """Processes a directory of a series of PDF/XRD patterns into a usable format. - - Constructs a 2d array out of a directory of PDF/XRD patterns containing each files dependent variable - column in a new column. Constructs a 1d array containing the grid values. - - Parameters - ---------- - file_path: str or Path object, optional - The path to the directory containing the input XRD/PDF data. If no path is specified, defaults to the - current working directory. Accepts a string or a pathlib.Path object. Input data not on the same grid - as the first file read will be ignored. - - Returns - ------- - tuple - The tuple whose first element is an R x M 2d array made of PDF/XRD patterns as each column; R is the - length of the signal and M is the number of patterns. The tuple contains a 1d array containing the values - of the grid points as its second element; Has length R. - - """ - - if file_path is None: - directory_path = Path.cwd() - else: - directory_path = Path(file_path) - - values_list = [] - grid_list = [] - current_grid = [] - for item in directory_path.iterdir(): - if item.is_file(): - data = loadData(item.resolve()) - if current_grid and current_grid != data[:, 0]: - print(f"{item.name} was ignored as it is not on a compatible grid.") - continue - else: - grid_list.append(data[:, 0]) - current_grid = grid_list[-1] - values_list.append(data[:, 1]) - - grid_array = np.column_stack(grid_list) - grid_vector = np.unique(grid_array, axis=1) - values_array = np.column_stack(values_list) - return grid_vector, values_array diff --git a/src/diffpy/snmf/optimizers.py b/src/diffpy/snmf/optimizers.py deleted file mode 100644 index d1a8e51f..00000000 --- a/src/diffpy/snmf/optimizers.py +++ /dev/null @@ -1,54 +0,0 @@ -import numpy as np -from scipy.optimize import minimize - - -def get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound): - """Finds the weights of stretched component signals under a two-sided constraint - - Solves min J(y) = (linear_coefficient)' * y + (1/2) * y' * (quadratic coefficient) * y where - lower_bound <= y <= upper_bound and stretched_component_gram_matrix is symmetric positive definite. - Finds the weightings of stretched component signals under a two-sided constraint. - - Parameters - ---------- - stretched_component_gram_matrix: 2d array like - The Gram matrix constructed from the stretched component matrix. It is a square positive definite matrix. - It has dimensions C x C where C is the number of component signals. Must be symmetric positive definite. - - linear_coefficient: 1d array like - The vector containing the product of the stretched component matrix and the transpose of the observed - data matrix. Has length C. - - lower_bound: 1d array like - The lower bound on the values of the output weights. Has the same dimensions of the function output. Each - element in 'lower_bound' determines the minimum value the corresponding element in the function output may - take. - - upper_bound: 1d array like - The upper bound on the values of the output weights. Has the same dimensions of the function output. Each - element in 'upper_bound' determines the maximum value the corresponding element in the function output may - take. - - Returns - ------- - 1d array like - The vector containing the weightings of the components needed to reconstruct a given input signal from the - input set. Has length C - - """ - - stretched_component_gram_matrix = np.asarray(stretched_component_gram_matrix) - linear_coefficient = np.asarray(linear_coefficient) - upper_bound = np.asarray(upper_bound) - lower_bound = np.asarray(lower_bound) - - # Set dynamic bounds based on the size of the linear coefficient - bounds = [(lower_bound, upper_bound) for _ in range(len(linear_coefficient))] - initial_guess = np.zeros_like(linear_coefficient) - - # Find optimal weights of linear coefficients - def obj_func(y): - return linear_coefficient.T @ y + 0.5 * y.T @ stretched_component_gram_matrix @ y - - result = minimize(obj_func, initial_guess, method="L-BFGS-B", bounds=bounds) - return result.x diff --git a/src/diffpy/snmf/polynomials.py b/src/diffpy/snmf/polynomials.py deleted file mode 100644 index c54ff215..00000000 --- a/src/diffpy/snmf/polynomials.py +++ /dev/null @@ -1,34 +0,0 @@ -import numpy as np - - -def compute_root(linear_coefficient, constant_term): - """ - Returns the largest real root of x^3+(linear_coefficient) * x + constant_term. If there are no real roots - return 0. - - Parameters - ---------- - linear_coefficient: ndarray like of floats - The matrix coefficient of the linear term - constant_term: 0d array like, 1d array like of floats or scalar - The constant scalar term of the problem - - Returns - ------- - ndarray of floats - The largest real root of x^3+(linear_coefficient) * x + constant_term if roots are real, else - return 0 array - - """ - linear_coefficient = np.asarray(linear_coefficient) - constant_term = np.asarray(constant_term) - solution = np.empty_like(linear_coefficient, dtype=np.float64) - - for index, value in np.ndenumerate(linear_coefficient): - inputs = [1, 0, value, constant_term] - roots = np.roots(inputs) - if ((constant_term / 2) ** 2 + (value / 3) ** 3) < 0: # Discriminant of depressed cubic equation - solution[index] = max(np.real(roots)) - else: - solution[index] = 0 - return solution diff --git a/src/diffpy/snmf/stretchednmfapp.py b/src/diffpy/snmf/stretchednmfapp.py deleted file mode 100644 index f577b887..00000000 --- a/src/diffpy/snmf/stretchednmfapp.py +++ /dev/null @@ -1,64 +0,0 @@ -import argparse -from pathlib import Path - -from diffpy.snmf.io import initialize_variables, load_input_signals -from diffpy.snmf.subroutines import initialize_components, lift_data - -ALLOWED_DATA_TYPES = ["powder_diffraction", "pd", "pair_distribution_function", "pdf"] - - -def create_parser(): - parser = argparse.ArgumentParser( - prog="stretched_nmf", description="Stretched Nonnegative Matrix Factorization" - ) - parser.add_argument( - "-i", - "--input-directory", - type=str, - default=None, - help="Directory containing experimental data. Defaults to current working directory.", - ) - parser.add_argument( - "-o", - "--output-directory", - type=str, - help="The directory where the results will be written. Defaults to '/snmf_results'.", - ) - parser.add_argument( - "-t", - "--data-type", - type=str, - default=None, - choices=ALLOWED_DATA_TYPES, - help="The type of the experimental data.", - ) - parser.add_argument( - "-l", - "--lift-factor", - type=float, - default=1, - help="The lifting factor. Data will be lifted by lifted_data = data + abs(min(data) * lift). Default 1.", - ) - parser.add_argument( - "number-of-components", - type=int, - help="The number of component signals for the NMF decomposition. Must be an integer greater than 0", - ) - parser.add_argument("-v", "--version", action="version", help="Print the software version number") - args = parser.parse_args() - return args - - -def main(): - args = create_parser() - if args.input_directory is None: - args.input_directory = Path.cwd() - grid, input_data = load_input_signals(args.input_directory) - lifted_input_data = lift_data(input_data, args.lift_factor) - variables = initialize_variables(lifted_input_data, args.number_of_components, args.data_type) - components = initialize_components(variables["number_of_components"], variables["number_of_signals"], grid) - return components - - -if __name__ == "__main__": - main() diff --git a/src/diffpy/snmf/subroutines.py b/src/diffpy/snmf/subroutines.py deleted file mode 100644 index ae7f7ef7..00000000 --- a/src/diffpy/snmf/subroutines.py +++ /dev/null @@ -1,494 +0,0 @@ -import numdifftools -import numpy as np - -from diffpy.snmf.containers import ComponentSignal -from diffpy.snmf.factorizers import lsqnonneg -from diffpy.snmf.optimizers import get_weights - - -def initialize_components(number_of_components, number_of_signals, grid_vector): - """Initializes ComponentSignals for each of the components in the decomposition. - - Parameters - ---------- - number_of_components: int - The number of component signals in the NMF decomposition - number_of_signals: int - grid_vector: 1d array - The grid of the user provided signals. - - Returns - ------- - tuple of ComponentSignal objects - The tuple containing `number_of_components` of initialized ComponentSignal objects. - """ - if number_of_components <= 0: - raise ValueError(f"Number of components = {number_of_components}. Number_of_components must be >= 1.") - components = list() - for component in range(number_of_components): - component = ComponentSignal(grid_vector, number_of_signals, component) - components.append(component) - return tuple(components) - - -def lift_data(data_input, lift=1): - """Lifts values of data_input. - - Adds 'lift' * the minimum value in data_input to data_input element-wise. - - Parameters - ---------- - data_input: 2d array like - The matrix containing a series of signals to be decomposed. Has dimensions N x M where N is the length - of each signal and M is the number of signals. - - lift: float - The factor representing how much to lift 'data_input'. - - Returns - ------- - 2d array like - The matrix that contains data_input - (min(data_input) * lift). - """ - data_input = np.asarray(data_input) - return data_input + np.abs(np.min(data_input) * lift) - - -def construct_stretching_matrix(components, number_of_components, number_of_signals): - """Constructs the stretching factor matrix. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the component signals in ComponentSignal objects. - number_of_signals: int - The number of signals in the data provided by the user. - - Returns - ------- - 2d array - The matrix containing the stretching factors for the component signals for each of the signals in the - raw data. Has dimensions `component_signal` x `number_of_signals` - """ - if (len(components)) == 0: - raise ValueError(f"Number of components = {number_of_components}. Number_of_components must be >= 1.") - number_of_components = len(components) - - if number_of_signals <= 0: - raise ValueError(f"Number of signals = {number_of_signals}. Number_of_signals must be >= 1.") - - stretching_factor_matrix = np.zeros((number_of_components, number_of_signals)) - for i, component in enumerate(components): - stretching_factor_matrix[i] = component.stretching_factors - return stretching_factor_matrix - - -def construct_component_matrix(components): - """Constructs the component matrix. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the component signals in ComponentSignal objects. - - Returns - ------- - 2d array - The matrix containing the component signal values. Has dimensions `signal_length` x `number_of_components`. - """ - signal_length = len(components[0].iq) - number_of_components = len(components) - if signal_length == 0: - raise ValueError(f"Signal length = {signal_length}. Signal length must be >= 1") - if number_of_components == 0: - raise ValueError(f"Number of components = {number_of_components}. Number_of_components must be >= 1") - - component_matrix = np.zeros((number_of_components, signal_length)) - for i, component in enumerate(components): - component_matrix[i] = component.iq - return component_matrix - - -def construct_weight_matrix(components): - """Constructs the weights matrix. - - Constructs a ΔΆ x M matrix where K is the number of components and M is the - number of signals. Each element is the stretching factor for a specific - weights for a specific signal from the data input. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the component signals. - - Returns - ------- - 2d array like - The 2d array containing the weightings for each component for each signal. - """ - number_of_components = len(components) - number_of_signals = len(components[0].weights) - if number_of_components == 0: - raise ValueError(f"Number of components = {number_of_components}. Number of components must be >= 1") - if number_of_signals == 0: - raise ValueError(f"Number of signals = {number_of_signals}. Number_of_signals must be >= 1.") - weights_matrix = np.zeros((number_of_components, number_of_signals)) - for i, component in enumerate(components): - weights_matrix[i] = component.weights - return weights_matrix - - -def update_weights(components, data_input, method=None): - """Updates the weights matrix. - - Updates the weights matrix and the weights vector for each ComponentSignal object. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the component signals. - method: str - The string specifying which method should be used to find a new weight matrix: non-negative least squares - or a quadratic program. - data_input: 2d array - The 2d array containing the user-provided signals. - - Returns - ------- - 2d array - The 2d array containing the weight factors for each component for each signal from `data_input`. - Has dimensions K x M where K is the number of components and M is the number of signals in `data_input.` - """ - data_input = np.asarray(data_input) - weight_matrix = construct_weight_matrix(components) - number_of_signals = len(components[0].weights) - number_of_components = len(components) - signal_length = len(components[0].grid) - for signal in range(number_of_signals): - stretched_components = np.zeros((signal_length, number_of_components)) - for i, component in enumerate(components): - stretched_components[:, i] = component.apply_stretch(signal)[0] - if method == "align": - weights = lsqnonneg(stretched_components, data_input[:, signal]) - else: - weights = get_weights( - stretched_components.T @ stretched_components, - -stretched_components.T @ data_input[:, signal], - 0, - 1, - ) - weight_matrix[:, signal] = weights - return weight_matrix - - -def reconstruct_signal(components, signal_idx): - """Reconstructs a specific signal from its weighted and stretched components. - - Calculates the linear combination of stretched components where each term is the stretched component multiplied - by its weight factor. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the ComponentSignal objects - signal_idx: int - The index of the specific signal in the input data to be reconstructed - - Returns - ------- - 1d array like - The reconstruction of a signal from calculated weights, stretching factors, and iq values. - """ - signal_length = len(components[0].grid) - reconstruction = np.zeros(signal_length) - for component in components: - stretched = component.apply_stretch(signal_idx)[0] - stretched_and_weighted = component.apply_weight(signal_idx, stretched) - reconstruction += stretched_and_weighted - return reconstruction - - -def initialize_arrays(number_of_components, number_of_moments, signal_length): - """Generates the initial guesses for the weight, stretching, and component matrices. - - Calculates the initial guesses for the component matrix, stretching factor matrix, and weight matrix. The - initial guess for the component matrix is a random (signal_length) x (number_of_components) matrix where - each element is between 0 and 1. The initial stretching factor matrix is a random - (number_of_components) x (number_of_moments) matrix where each element is number slightly perturbed from 1. - The initial weight matrix guess is a random (number_of_components) x (number_of_moments) matrix where - each element is between 0 and 1. - - Parameters - ---------- - number_of_components: int - The number of component signals to obtain from the stretched nmf decomposition. - - number_of_moments: int - The number of signals in the user provided dataset where each signal is at a different moment. - - signal_length: int - The length of each signal in the user provided dataset. - - Returns - ------- - tuple of 2d arrays of floats - The tuple containing three elements: the initial component matrix guess, the initial stretching factor matrix - guess, and the initial weight factor matrix guess in that order. - """ - component_matrix_guess = np.random.rand(signal_length, number_of_components) - weight_matrix_guess = np.random.rand(number_of_components, number_of_moments) - stretching_matrix_guess = ( - np.ones(number_of_components, number_of_moments) - + np.random.randn(number_of_components, number_of_moments) * 1e-3 - ) - return component_matrix_guess, weight_matrix_guess, stretching_matrix_guess - - -def objective_function( - residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, sparsity -): - """Defines the objective function of the algorithm and returns its value. - - Calculates the value of '(||residual_matrix||_F) ** 2 + smoothness * (||smoothness_term * - stretching_factor_matrix.T||)**2 + sparsity * sum(component_matrix ** .5)' and returns its value. - - Parameters - ---------- - residual_matrix: 2d array like - The matrix where each column is the difference between an experimental PDF/XRD pattern and a calculated - PDF/XRD pattern at each grid point. Has dimensions R x M where R is the length of each pattern and M is - the amount of patterns. - - stretching_factor_matrix: 2d array like - The matrix containing the stretching factors of the calculated component signal. Has dimensions K x M where - K is the amount of components and M is the number of experimental PDF/XRD patterns. - - smoothness: float - The coefficient of the smoothness term which determines the intensity of the smoothness term and its - behavior. It is not very sensitive and is usually adjusted by multiplying it by ten. - - smoothness_term: 2d array like - The regularization term that ensures that smooth changes in the component stretching signals are favored. - Has dimensions (M-2) x M where M is the amount of experimentally obtained PDF/XRD patterns, the moment - amount. - - component_matrix: 2d array like - The matrix containing the calculated component signals of the experimental PDF/XRD patterns. Has dimensions - R x K where R is the signal length and K is the number of component signals. - - sparsity: float - The parameter determining the intensity of the sparsity regularization term which enables the algorithm to - exploit the sparse nature of XRD data. It is usually adjusted by doubling. - - Returns - ------- - float - The value of the objective function. - """ - residual_matrix = np.asarray(residual_matrix) - stretching_factor_matrix = np.asarray(stretching_factor_matrix) - component_matrix = np.asarray(component_matrix) - return ( - 0.5 * np.linalg.norm(residual_matrix, "fro") ** 2 - + 0.5 * smoothness * np.linalg.norm(smoothness_term @ stretching_factor_matrix.T, "fro") ** 2 - + sparsity * np.sum(np.sqrt(component_matrix)) - ) - - -def get_stretched_component(stretching_factor, component, signal_length): - """Applies a stretching factor to a component signal. - - Computes a stretched signal and reinterpolates it onto the original grid of points. Uses a normalized grid - of evenly spaced integers counting from 0 to signal_length (exclusive) to approximate values in between grid - nodes. Once this grid is stretched, values at grid nodes past the unstretched signal's domain are set to zero. - Returns the approximate values of x(r/a) from x(r) where x is a component signal. - - Parameters - ---------- - stretching_factor: float - The stretching factor of a component signal at a particular moment. - component: 1d array like - The calculated component signal without stretching or weighting. Has length N, the length of the signal. - signal_length: int - The length of the component signal. - - Returns - ------- - tuple of 1d array of floats - The calculated component signal with stretching factors applied. Has length N, the length of the unstretched - component signal. Also returns the gradient and hessian of the stretching transformation. - """ - component = np.asarray(component) - normalized_grid = np.arange(signal_length) - - def stretched_component_func(stretching_factor): - return np.interp(normalized_grid / stretching_factor, normalized_grid, component, left=0, right=0) - - derivative_func = numdifftools.Derivative(stretched_component_func) - second_derivative_func = numdifftools.Derivative(derivative_func) - - stretched_component = stretched_component_func(stretching_factor) - stretched_component_gra = derivative_func(stretching_factor) - stretched_component_hess = second_derivative_func(stretching_factor) - - return ( - np.asarray(stretched_component), - np.asarray(stretched_component_gra), - np.asarray(stretched_component_hess), - ) - - -def update_weights_matrix( - component_amount, - signal_length, - stretching_factor_matrix, - component_matrix, - data_input, - moment_amount, - weights_matrix, - method, -): - """Update the weight factors matrix. - - Parameters - ---------- - component_amount: int - The number of component signals the user would like to determine from the experimental data. - - signal_length: int - The length of the experimental signal patterns - - stretching_factor_matrix: 2d array like - The matrx containing the stretching factors of the calculated component signals. Has dimensions K x M - where K is the number of component signals and M is the number of XRD/PDF patterns. - - component_matrix: 2d array like - The matrix containing the unstretched calculated component signals. Has dimensions N x K where N is the - length of the signals and K is the number of component signals. - - data_input: 2d array like - The experimental series of PDF/XRD patterns. Has dimensions N x M where N is the length of the PDF/XRD - signals and M is the number of PDF/XRD patterns. - - moment_amount: int - The number of PDF/XRD patterns from the experimental data. - - weights_matrix: 2d array like - The matrix containing the weights of the stretched component signals. Has dimensions K x M where K is - the number of component signals and M is the number of XRD/PDF patterns. - - method: str - The string specifying the method for obtaining individual weights. - - Returns - ------- - 2d array like - The matrix containing the new weight factors of the stretched component signals. - """ - stretching_factor_matrix = np.asarray(stretching_factor_matrix) - component_matrix = np.asarray(component_matrix) - data_input = np.asarray(data_input) - weights_matrix = np.asarray(weights_matrix) - weight = np.zeros(component_amount) - for i in range(moment_amount): - stretched_components = np.zeros((signal_length, component_amount)) - for n in range(component_amount): - stretched_components[:, n] = get_stretched_component( - stretching_factor_matrix[n, i], component_matrix[:, n], signal_length - )[0] - if method == "align": - weight = lsqnonneg(stretched_components[0:signal_length, :], data_input[0:signal_length, i]) - else: - weight = get_weights( - stretched_components[0:signal_length, :].T @ stretched_components[0:signal_length, :], - -1 * stretched_components[0:signal_length, :].T @ data_input[0:signal_length, i], - 0, - 1, - ) - weights_matrix[:, i] = weight - return weights_matrix - - -def get_residual_matrix( - component_matrix, weights_matrix, stretching_matrix, data_input, moment_amount, component_amount, signal_length -): - """Obtains the residual matrix between the experimental data and calculated data. - - Calculates the difference between the experimental data and the reconstructed experimental data created from - the calculated components, weights, and stretching factors. For each experimental pattern, the stretched and - weighted components making up that pattern are subtracted. - - Parameters - ---------- - component_matrix: 2d array like - The matrix containing the calculated component signals. Has dimensions N x K where N is the length of the - signal and K is the number of calculated component signals. - - weights_matrix: 2d array like - The matrix containing the calculated weights of the stretched component signals. Has dimensions K x M where - K is the number of components and M is the number of moments or experimental PDF/XRD patterns. - - stretching_matrix: 2d array like - The matrix containing the calculated stretching factors of the calculated component signals. Has dimensions - K x M where K is the number of components and M is the number of moments or experimental PDF/XRD patterns. - - data_input: 2d array like - The matrix containing the experimental PDF/XRD data. Has dimensions N x M where N is the length of the - signals and M is the number of signal patterns. - - moment_amount: int - The number of patterns in the experimental data. Represents the number of moments in time in the data series - - component_amount: int - The number of component signals the user would like to obtain from the experimental data. - - signal_length: int - The length of the signals in the experimental data. - - - Returns - ------- - 2d array like - The matrix containing the residual between the experimental data and reconstructed data from calculated - values. Has dimensions N x M where N is the signal length and M is the number of moments. Each column - contains the difference between an experimental signal and a reconstruction of that signal from the - calculated weights, components, and stretching factors. - """ - component_matrix = np.asarray(component_matrix) - weights_matrix = np.asarray(weights_matrix) - stretching_matrix = np.asarray(stretching_matrix) - data_input = np.asarray(data_input) - residual_matrx = -1 * data_input - for m in range(moment_amount): - residual = residual_matrx[:, m] - for k in range(component_amount): - residual = ( - residual - + weights_matrix[k, m] - * get_stretched_component(stretching_matrix[k, m], component_matrix[:, k], signal_length)[0] - ) - residual_matrx[:, m] = residual - return residual_matrx - - -def reconstruct_data(components): - """Reconstructs the `input_data` matrix. - - Reconstructs the `input_data` matrix from calculated component signals, weights, and stretching factors. - - Parameters - ---------- - components: tuple of ComponentSignal objects - The tuple containing the component signals. - - Returns - ------- - 2d array - The 2d array containing the reconstruction of input_data. - """ - signal_length = len(components[0].iq) - number_of_signals = len(components[0].weights) - data_reconstruction = np.zeros((signal_length, number_of_signals)) - for signal in range(number_of_signals): - data_reconstruction[:, signal] = reconstruct_signal(components, signal) - return data_reconstruction diff --git a/tests/test_containers.py b/tests/test_containers.py deleted file mode 100644 index 9595ae51..00000000 --- a/tests/test_containers.py +++ /dev/null @@ -1,131 +0,0 @@ -import numpy as np -import pytest - -from diffpy.snmf.containers import ComponentSignal - - -@pytest.mark.parametrize( - "grid, number_of_signals, id_number, iq, stretching_factor, expected", - [ - ( - np.arange(10), - 3, - 0, - [6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], - 0.25, - [ - [6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], - [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], - [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0], - ], - ), - ( - np.arange(5), - 10, - 0, - [-11.47, -10.688, -8.095, -29.44, 14.38], - 1.25, - [ - [-11.47, -10.8444, -9.1322, -16.633, -20.6760], - [0, -0.50048, -3.31904, 40.9824, -112.1792], - [0, 0.800768, 5.310464, -65.57184, 179.48672], - ], - ), - ( - np.arange(5), - 2, - 0, - [-11.47, -10.688, -8.095, -29.44, 14.38], - 0.88, - [ - [-11.47, -10.3344, -13.9164, -11.5136, 0], - [0, -3.3484, 55.1265, -169.7572, 0], - [0, 7.609997, -125.2876, 385.81189, 0], - ], - ), - ( - np.arange(10), - 1, - 2, - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 0.88, - [ - [1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], - [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], - [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0], - ], - ), - ( - np.arange(14), - 100, - 3, - [ - -2.9384, - -1.4623, - -2.0913, - 4.6304, - -1.2127, - 1.4737, - -0.3791, - 1.7506, - -1.5068, - -2.7625, - 0.9617, - -0.3494, - -0.3862, - 2.7960, - ], - 0.55, - [ - [-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0], - [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], - [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0], - ], - ), - ( - np.arange(11), - 20, - 4, - [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], - 0.987, - [ - [0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], - [0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], - [0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0], - ], - ), - ( - np.arange(9), - 15, - 3, - [-1, -2, -3, -4, -5, -6, -7, -8, -9], - -0.4, - [[-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], - ), - ], -) -def test_apply_stretch(grid, number_of_signals, id_number, iq, stretching_factor, expected): - component = ComponentSignal(grid, number_of_signals, id_number) - component.iq = iq - component.stretching_factors[0] = stretching_factor - actual = component.apply_stretch(0) - np.testing.assert_allclose(actual, expected, rtol=1e-01) - - -@pytest.mark.parametrize( - "grid, number_of_signals, id_number, iq, weight, expected", - [ - (np.arange(5), 2, 0, [0, 1, 2, 3, 4], 0.5, [0, 0.5, 1, 1.5, 2]), - (np.arange(5), 20, 2, [0, -1, -2, -3, -4], 0.25, [0, -0.25, -0.5, -0.75, -1]), - (np.arange(40), 200, 4, np.arange(0, 10, 0.25), 0.3, np.arange(0, 10, 0.25) * 0.3), - (np.arange(1), 10, 2, [10.5, 11.5, -10.5], 0, [0, 0, 0]), - ([-12, -10, -15], 5, 2, [-0.5, -1, -1.2], 0.9, [-0.45, -0.9, -1.08]), - ([-12, -10, -15], 5, 2, [0, 0, 0], 0.9, [0, 0, 0]), - ], -) -def test_apply_weight(grid, number_of_signals, id_number, iq, weight, expected): - component = ComponentSignal(grid, number_of_signals, id_number) - component.iq = np.array(iq) - component.weights[0] = weight - actual = component.apply_weight(0) - np.testing.assert_allclose(actual, expected, rtol=1e-04) diff --git a/tests/test_factorizers.py b/tests/test_factorizers.py deleted file mode 100644 index 6b8efa07..00000000 --- a/tests/test_factorizers.py +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -import pytest - -from diffpy.snmf.factorizers import lsqnonneg - - -@pytest.mark.parametrize( - "stretched_component_matrix, target_signal, expected", - [ - ([[1, 0], [1, 0], [0, 1]], [2, 1, 1], [1.5, 1.0]), - ([[2, 3], [1, 2], [0, 0]], [7, 7, 2], [0, 2.6923]), - ([[3, 2, 4, 1]], [3.2], [0, 0, 0.8, 0]), - ([[-0.4, 0], [0, 0], [-9, -18]], [-2, -3, -4.9], [0.5532, 0]), - ([[-0.1, -0.2], [-0.8, -0.9]], [0, 0], [0, 0]), - ([[0, 0], [0, 0]], [10, 10], [0, 0]), - ([[2], [1], [-4], [-0.3]], [6, 4, 0.33, -5], 0.767188240872451), - ], -) -def test_lsqnonneg(stretched_component_matrix, target_signal, expected): - actual = lsqnonneg(stretched_component_matrix, target_signal) - np.testing.assert_array_almost_equal(actual, expected, decimal=4) diff --git a/tests/test_optimizers.py b/tests/test_optimizers.py deleted file mode 100644 index 31799150..00000000 --- a/tests/test_optimizers.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -from diffpy.snmf.optimizers import get_weights - - -@pytest.mark.parametrize( - "stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound, expected", - [ - ([[1, 0], [0, 1]], [1, 1], 0, 0, [0, 0]), - ([[1, 0], [0, 1]], [1, 1], -1, 1, [-1, -1]), - ([[1.75, 0], [0, 1.5]], [1, 1.2], -1, 1, [-0.571428571428571, -0.8]), - ([[0.75, 0.2], [0.2, 0.75]], [-0.1, -0.2], -1, 1, [0.066985645933014, 0.248803827751196]), - ([[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, 1, 1], -10, 12, [-1.5, -2, -1.5]), - ([[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, -1, -1], -10, 12, [0, 1, 1]), - ([[4, 0, 0, 0], [0, 3, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]], [-2, -3, -4, -1], 0, 1000, [0.5, 1, 2, 1]), - ], -) -def test_get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound, expected): - actual = get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound) - assert actual == pytest.approx(expected, rel=1e-4, abs=1e-6) diff --git a/tests/test_polynomials.py b/tests/test_polynomials.py deleted file mode 100644 index 51912f0e..00000000 --- a/tests/test_polynomials.py +++ /dev/null @@ -1,27 +0,0 @@ -import numpy as np -import pytest - -from diffpy.snmf.polynomials import compute_root - - -@pytest.mark.parametrize( - "linear_coefficient, constant, expected", - [ - (0, 0, 0), - (-99.99, 12.50, 9.936397678254531), - (-4, 0, 2), - (10, 0, 0), - (-7, -7, 3.04891734), - (100, 72, 0), - (1, 3, 0), - (0, -7, 0), - (-9, 0, 3), - (-9, 3, 2.8169), - ([2, 2], 2, [0, 0]), - ([[2, 2], [2, 2]], 2, [[0, 0], [0, 0]]), - ([[[3, 2], [-2, -2], [100, 0]]], 2, [[[0, 0], [0, 0], [0, 0]]]), - ], -) -def test_rooth(linear_coefficient, constant, expected): - actual = compute_root(linear_coefficient, constant) - np.testing.assert_allclose(actual, expected, rtol=1e-4) diff --git a/tests/test_subroutines.py b/tests/test_subroutines.py deleted file mode 100644 index 3b3e392c..00000000 --- a/tests/test_subroutines.py +++ /dev/null @@ -1,593 +0,0 @@ -import numpy as np -import pytest - -from diffpy.snmf.containers import ComponentSignal -from diffpy.snmf.subroutines import ( - construct_component_matrix, - construct_stretching_matrix, - construct_weight_matrix, - get_residual_matrix, - get_stretched_component, - initialize_components, - lift_data, - objective_function, - reconstruct_data, - reconstruct_signal, - update_weights, - update_weights_matrix, -) - - -@pytest.mark.parametrize( - "residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, sparsity, expected", - [ - ([[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 1, 2.574e14), - ([[11, 2], [31, 4]], [[5, 63], [7, 18]], 0.001, [[21, 2], [3, 4]], [[11, 22], [3, 40]], 1, 650.4576), - ([[1, 2], [3, 4]], [[5, 6], [7, 8]], 1e11, [[1, 2], [3, 4]], [[1, 2], [3, 4]], 0, 2.574e14), - ], -) -def test_objective_function( - residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, sparsity, expected -): - actual = objective_function( - residual_matrix, stretching_factor_matrix, smoothness, smoothness_term, component_matrix, sparsity - ) - assert actual == pytest.approx(expected) - - -@pytest.mark.parametrize( - "stretching_factor, component, signal_length, expected", - [ - ( - 0.25, - [6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], - 10, - ( - [6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0], - [0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0], - [0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0], - ), - ), - ( - 1.25, - [-11.47, -10.688, -8.095, -29.44, 14.38], - 5, - ( - [-11.47, -10.8444, -9.1322, -16.633, -20.6760], - [0, -0.50048, -3.31904, 40.9824, -112.1792], - [0, 0.800768, 5.310464, -65.57184, 179.48672], - ), - ), - ( - 0.88, - [-11.47, -10.688, -8.095, -29.44, 14.38], - 5, - ( - [-11.47, -10.3344, -13.9164, -11.5136, 0], - [0, -3.3484, 55.1265, -169.7572, 0], - [0, 7.609997, -125.2876, 385.81189, 0], - ), - ), - ( - 0.88, - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 10, - ( - [1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0], - [0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0], - [0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0], - ), - ), - ( - 0.55, - [ - -2.9384, - -1.4623, - -2.0913, - 4.6304, - -1.2127, - 1.4737, - -0.3791, - 1.7506, - -1.5068, - -2.7625, - 0.9617, - -0.3494, - -0.3862, - 2.7960, - ], - 14, - ( - [-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0], - [0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0], - [0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0], - ), - ), - ( - 0.987, - [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], - 11, - ( - [0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0], - [0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0], - [0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0], - ), - ), - ( - -0.4, - [-1, -2, -3, -4, -5, -6, -7, -8, -9], - 9, - ([-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]), - ), - ], -) -def test_get_stretched_component(stretching_factor, component, signal_length, expected): - actual = get_stretched_component(stretching_factor, component, signal_length) - np.testing.assert_allclose(actual, expected, rtol=1e-01) - - -@pytest.mark.parametrize( - "component_amount, signal_length, " - "stretching_factor_matrix, component_matrix, data, moment, weights_matrix, method, expected", - [ - ( - 2, - 2, - [[0.5, 0.6], [0.7, 0.8]], - [[1, 2], [4, 8]], - [[1.6, 2.8], [5, 8.8]], - 2, - [[0.78, 0.12], [0.5, 0.5]], - None, - [[0.533333, 0.933333], [0.533333, 0.933333]], - ), - ( - 2, - 3, - [[0.5], [0.5]], - [[1, 2.5], [1.5, 3], [2, 3.5]], - [[1, 2], [3, 4], [5, 6]], - 1, - [[0.5], [0.5]], - None, - [[1], [0.1892]], - ), - ( - 2, - 3, - [[0.5, 0.6, 0.7], [0.5, 0.6, 0.7]], - [[1, 2.5], [1.5, 3], [2, 3.5]], - [[1, 2, 3], [3, 4, 5], [5, 6, 7]], - 3, - [[0.5, 0.45, 0.4], [0.5, 0.45, 0.4]], - None, - [[1, 1, 1], [0.1892, 0.5600, 0.938]], - ), - ( - 3, - 3, - [[0.7, 0.8, 0.9], [0.71, 0.72, 0.73], [0.8, 0.85, 0.9]], - [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -0.5]], - [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], - 3, - [[0.9, 0.4, 0.5], [1, 0, 0.4], [0, 0, 0.98]], - None, - [[1.0, 0.0900485, 0.0], [0.585632, 0.497497, 0.179719], [0.0, 0.52223655, 1.0]], - ), - (2, 2, [[0.5], [0.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[0.6], [0.4]], "align", [[0], [0]]), - (1, 3, [[0.5, 0.3]], [[1], [1.1], [1.3]], [[1, 2], [2, 3], [3, 2]], 2, [[0.6, 0.4]], None, [[1, 1]]), - ( - 2, - 2, - [[0.5, 0.6], [0.7, 0.8]], - [[1, 2], [4, 8]], - [[1.6, 2.8], [5, 8.8]], - 2, - [[0.78, 0.12], [0.5, 0.5]], - "align", - [[0, 0], [0.8, 1.4]], - ), - ( - 2, - 3, - [[0.5], [0.5]], - [[1, 2.5], [1.5, 3], [2, 3.5]], - [[1, 2], [3, 4], [5, 6]], - 1, - [[0.5], [0.5]], - "align", - [[1.4], [0]], - ), - ( - 3, - 3, - [[0.7, 0.8, 0.9], [0.71, 0.72, 0.73], [0.8, 0.85, 0.9]], - [[-1, -2.7, -3], [-11, -6, -5.1], [0, -1, -0.5]], - [[-2, -3, -4], [-9, -5, -5], [0, -2, -1]], - 3, - [[0.9, 0.4, 0.5], [1, 0, 0.4], [0, 0, 0.98]], - "align", - [[1.281265, 0.104355, 0], [0.0, 0.0, 0.0], [0.239578, 0.965215, 1.162571]], - ), - (2, 2, [[0.5], [0.5]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], 1, [[0.6], [0.4]], "align", [[0], [0]]), - ( - 1, - 3, - [[0.5, 0.3]], - [[1], [1.1], [1.3]], - [[1, 2], [2, 3], [3, 2]], - 2, - [[0.6, 0.4]], - "align", - [[1.3383, 2]], - ), - ], -) -def test_update_weights_matrix( - component_amount, - signal_length, - stretching_factor_matrix, - component_matrix, - data, - moment, - weights_matrix, - method, - expected, -): - actual = update_weights_matrix( - component_amount, - signal_length, - stretching_factor_matrix, - component_matrix, - data, - moment, - weights_matrix, - method, - ) - - np.testing.assert_allclose(actual, expected, rtol=1e-03, atol=1e-05) - - -@pytest.mark.parametrize( - "component_matrix, weights_matrix, stretching_matrix, data_input, moment_amount, " - "component_amount, signal_length, expected", - [ - ( - [[1, 2], [3, 4]], - [[0.25], [0.75]], - [[0.9], [0.7]], - [[11, 22], [33, 44]], - 1, - 2, - 2, - [[-9, -22], [-33, -44]], - ), - ([[1, 2], [3, 4]], [[1], [1]], [[1], [1]], [[11, 22], [33, 44]], 1, 2, 2, [[-8, -22], [-26, -44]]), - ( - [[1.1, 4.4], [1.2, 4.5], [14, 7.8]], - [[0.4, 0.6], [0.75, 0.25]], - [[0.9, 0.89], [0.98, 0.88]], - [[10, 20], [-10.5, -20.6], [0.6, 0.9]], - 2, - 2, - 3, - [[-6.26, -18.24], [14.9744, 23.5067], [-0.6, -0.9]], - ), - # positive float - ( - [[-1.1, -4.4], [-1.2, -4.5], [-14, -7.8]], - [[0.4, 0.6], [0.75, 0.25]], - [[0.9, 0.89], [0.98, 0.88]], - [[10, 20], [-10.5, -20.6], [0.6, 0.9]], - 2, - 2, - 3, - [[-13.74, -21.76], [6.0256, 17.6933], [-0.6, -0.9]], - ), - # negative floats - ( - [[0, 0, 0, 0], [0, 0, 0, 0]], - [[0.4], [0.2], [0.3], [0.3]], - [[0.9], [0.9], [0.9], [0.9]], - [[0, 0, 0, 0], [0, 0, 0, 0]], - 1, - 4, - 2, - [[0, 0, 0, 0], [0, 0, 0, 0]], - ), - ], -) -def test_get_residual_matrix( - component_matrix, - weights_matrix, - stretching_matrix, - data_input, - moment_amount, - component_amount, - signal_length, - expected, -): - actual = get_residual_matrix( - component_matrix, - weights_matrix, - stretching_matrix, - data_input, - moment_amount, - component_amount, - signal_length, - ) - np.testing.assert_allclose(actual, expected, rtol=1e-04) - - -@pytest.mark.parametrize( - "components", - [ - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ] - ), - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0)]), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 3), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 4), - ] - ), - # ([]) # Exception expected - ], -) -def test_reconstruct_data(components): - actual = reconstruct_data(components) - assert actual.shape == (len(components[0].iq), len(components[0].weights)) - - -@pytest.mark.parametrize( - "data_input, lift, expected", - [ - # Correct structure: Each test case should be a tuple with three elements.pt - ([[1, -1, 1], [0, 0, 0], [2, 10, -3]], 1, [[4, 2, 4], [3, 3, 3], [5, 13, 0]]), - ([[1, -1, 1], [0, 0, 0], [2, 10, -3]], 0, [[1, -1, 1], [0, 0, 0], [2, 10, -3]]), - ([[1, -1, 1], [0, 0, 0], [2, 10, -3]], 0.5, [[2.5, 0.5, 2.5], [1.5, 1.5, 1.5], [3.5, 11.5, -1.5]]), - ([[1, -1, 1], [0, 0, 0], [2, 10, -3]], -1, [[4, 2, 4], [3, 3, 3], [5, 13, 0]]), - ([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 100, [[0, 0, 0], [0, 0, 0], [0, 0, 0]]), - ([[1.5, 2], [10.5, 1], [0.5, 2]], 1, [[2, 2.5], [11, 1.5], [1, 2.5]]), - ([[-10, -10.5], [-12.2, -12.2], [0, 0]], 1, [[2.2, 1.7], [0, 0], [12.2, 12.2]]), - ], -) -def test_lift_data(data_input, lift, expected): - actual = lift_data(data_input, lift) - np.testing.assert_allclose(actual, expected) - - -@pytest.mark.parametrize( - "number_of_components, number_of_signals, grid_vector", - [ - (2, 3, [0, 0.5, 1, 1.5]), # Regular usage - # (0, 3,[0, .5, 1, 1.5]), # Zero components raise an exception. Not tested - ], -) -def test_initialize_componentstest_initialize_components(number_of_components, number_of_signals, grid_vector): - actual = initialize_components(number_of_components, number_of_signals, grid_vector) - assert len(actual) == number_of_components - assert len(actual[0].weights) == number_of_signals - assert (actual[0].grid == np.array(grid_vector)).all() - - -@pytest.mark.parametrize( - "components, number_of_components, number_of_signals", - [ - ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0)], 1, 20), - ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0)], 4, 20), - # ([ComponentSignal([0,.5,1,1.5],20,0)],0,20), # Raises an exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],-2,20), # Raises an exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],1,0), # Raises an Exception - # ([ComponentSignal([0,.5,1,1.5],20,0)],1,-3), # Raises an exception - ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 1.5], 20, 1)], 2, 20), - ([ComponentSignal([0, 0.5, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 21.5], 20, 1)], 2, 20), - ([ComponentSignal([0, 1, 1.5], 20, 0), ComponentSignal([0, 0.5, 1, 21.5], 20, 1)], 2, 20), - # ([ComponentSignal([0,.5,1,1.5],20,0),ComponentSignal([0,.5,1,1.5],20,1)],1,-3), - # Negative signal length. Raises an exception - # ([],1,20), # Empty components. Raises an Exception - # ([],-1,20), # Empty components with negative number of components. Raises an exception - # ([],0,20), # Empty components with zero number of components. Raises an exception - # ([],1,0), # Empty components with zero signal length. Raises an exception. - # ([],-1,-2), # Empty components with negative number of components and signal length Raises an exception. - ], -) -def test_construct_stretching_matrix(components, number_of_components, number_of_signals): - actual = construct_stretching_matrix(components, number_of_components, number_of_signals) - for component in components: - np.testing.assert_allclose(actual[component.id, :], component.stretching_factors) - # assert actual[component.id, :] == component.stretching_factors - - -@pytest.mark.parametrize( - "components", - [ - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0)]), - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 0, 0)]), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 2.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ([ComponentSignal([0.25], 20, 0), ComponentSignal([0.25], 20, 1), ComponentSignal([0.25], 20, 2)]), - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1)]), - # ([ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 0), - # ComponentSignal([[0, .25, .5, .75, 1],[0, .25, .5, .75, 1]], 20, 1)]), - # iq is multidimensional. Expected to fail - # (ComponentSignal([], 20, 0)), # Expected to fail - # ([]), #Expected to fail - ], -) -def test_construct_component_matrix(components): - actual = construct_component_matrix(components) - for component in components: - np.testing.assert_allclose(actual[component.id], component.iq) - - -@pytest.mark.parametrize( - "components", - [ - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0)]), - # ([ComponentSignal([0,.25,.5,.75,1],0,0)]), # 0 signal length. Failure expected - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), - ComponentSignal([0, 0.25, 0.5, 2.75, 1], 20, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 2), - ] - ), - ([ComponentSignal([0.25], 20, 0), ComponentSignal([0.25], 20, 1), ComponentSignal([0.25], 20, 2)]), - ([ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 0), ComponentSignal([0, 0.25, 0.5, 0.75, 1], 20, 1)]), - # (ComponentSignal([], 20, 0)), # Expected to fail - # ([]), #Expected to fail - ], -) -def test_construct_weight_matrix(components): - actual = construct_weight_matrix(components) - for component in components: - np.testing.assert_allclose(actual[component.id], component.weights) - - -@pytest.mark.parametrize( - "components, data_input, method", - [ - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], - None, - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], - "align", - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], - None, - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], - "align", - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[-0.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, 0.1]], - None, - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - [[-0.5, 1], [1.2, -1.3], [1.1, -1], [0, -1.5], [0, 0.1]], - "align", - ), - # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), - # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [[1, 1], [1.2, 1.3], [1.3, 1.4], [1.4, 1.5], [2, 2.1]], - # None), - # ([ComponentSignal([0, .25, .5, .75, 1], 0, 0), ComponentSignal([0, .25, .5, .75, 1], 0, 1), - # ComponentSignal([0, .25, .5, .75, 1], 0, 2)], [], None), - # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], [], 170), - ], -) -def test_update_weights(components, data_input, method): - actual = update_weights(components, data_input, method) - assert np.shape(actual) == (len(components), len(components[0].weights)) - - -@pytest.mark.parametrize( - "components, expected", - [ - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - 1, - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 2, 2), - ], - 0, - ), - ( - [ - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 0), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 1), - ComponentSignal([0, 0.25, 0.5, 0.75, 1], 3, 2), - ], - 2, - ), - # ([ComponentSignal([0, .25, .5, .75, 1], 2, 0), ComponentSignal([0, .25, .5, .75, 1], 2, 1), - # ComponentSignal([0, .25, .5, .75, 1], 2, 2)], -1), - ], -) -def test_reconstruct_signal(components, expected): - actual = reconstruct_signal(components, expected) - assert len(actual) == len(components[0].grid)