Source code for metamorphic_relations.MR
from metamorphic_relations.Data import Data
from metamorphic_relations.Transform import Transform
import numpy as np
[docs]class MR:
"""
Creates an object to represent the tree of all combinations of the transforms
:param transforms: a list of transforms
:param max_composite: the maximum number of transformations that can be performed sequentially on each data element
"""
def __init__(self, transforms: list[Transform], max_composite: int = 1):
self.transforms = transforms
self.MR_tree = None
self.MR_list = None
self.MR_list_names = None
self.update_composite(max_composite)
[docs] def update_composite(self, max_composite: int):
"""
Updates the tree based on the current list and max_composite
:param max_composite: the maximum number of transformations that can be performed sequentially on each data element
"""
self.MR_tree = self.get_composite_tree(max_composite)
self.MR_list = self.get_composite_list()
self.MR_list_names = self.get_composite_list_names()
[docs] def get_composite_tree(self, max_composite: int) -> list[tuple[Transform, list]]:
"""
Gets the tree of composite transforms
:param max_composite: the maximum number of transformations that can be performed sequentially on each data element
:return: The tree of composite transforms
"""
if max_composite <= 0:
raise Exception("max_composite must be positive")
max_composite = min(len(self.transforms), max_composite)
composite_MRs = [(self.transforms[i], self.add_composite(max_composite - 1, [i], self.transforms[i].target)) for
i in range(len(self.transforms))]
return composite_MRs
[docs] def add_composite(self, max_composite: int, used_indices: list[int], prev_y: int) -> list[tuple[Transform, list]]:
"""
Adds a branch to the tree of composite transforms
:param max_composite: the maximum number of remaining transformations that can be performed sequentially given the previous transforms more shallow in the tree
:param used_indices: the transforms that have already been used in this branch
:param prev_y: the final y value after the previous transform
:return: the tree from this point of possible combinations of transforms
"""
if max_composite == 0:
return []
if prev_y == -1:
return [(self.transforms[i],
self.add_composite(max_composite - 1, [i] + used_indices, self.transforms[i].target))
for i in range(len(self.transforms)) if i not in used_indices]
return [
(self.transforms[i], self.add_composite(max_composite - 1, [i] + used_indices, self.transforms[i].target))
for i in range(len(self.transforms)) if
i not in used_indices and (self.transforms[i].current == prev_y or self.transforms[i].current == -1)]
[docs] def get_composite_list(self, MR_tree: list[tuple[Transform, list]] = None) -> list[tuple[Transform, list]]:
"""
Takes a tree of MRs and converts it to a list of paths through the tree
:param MR_tree: the tree to convert
:return: a list of trees each of which has a single path from root to leaf
"""
if MR_tree is None:
MR_tree = self.MR_tree
MR_list = []
for i in range(len(MR_tree)):
if len(MR_tree[i][1]) <= 1:
MR_list.append(MR_tree[i])
else:
MR_list += [(MR_tree[i][0], [ts]) for ts in self.get_composite_list(MR_tree[i][1])]
return MR_list
[docs] def get_composite_list_names(self) -> list[str]:
"""
Gets the names of each of the composite MRs
:return: a list of string names
"""
MR_list_names = []
for lst in self.MR_list:
MR_list_names.append(self.get_composite_name(lst))
return MR_list_names
[docs] @staticmethod
def get_composite_name(transform_list: tuple[Transform, list]) -> str:
"""
Gets the name of a composite MR, by concatenating each Transform's individual name
:param transform_list: the composite MR representation
:return: a string name
"""
if len(transform_list[1]) == 0:
return transform_list[0].name
else:
return transform_list[0].name + " -> " + MR.get_composite_name(transform_list[1][0])
[docs] @staticmethod
def for_all_labels(transform, label_current_indices: list[int] = None, label_target_indices: list[int] = None,
name: str = None) -> list[Transform]:
"""
Adds transforms for a given set of labels
:param function transform: the transformation function
:param label_current_indices: the indices of labels to use this transform on (default leads to all labels)
:param label_target_indices: the indices of labels to give after the transform (default leads to labels remaining the same)
:param name: the name of the transform by default uses the function name
:return: a list of transforms
"""
if name is None:
name = transform.__str__()
MR_list = []
if label_current_indices is None:
MR_list.append(Transform(transform, -1, -1, name))
elif label_target_indices is None:
for i in label_current_indices:
MR_list.append(Transform(transform, i, i, name + " (" + str(i) + " to " + str(i) + ")"))
elif len(label_current_indices) == len(label_target_indices):
for i in range(len(label_current_indices)):
MR_list.append(Transform(transform, label_current_indices[i], label_target_indices[i], name + " (" + str(label_current_indices[i]) + " to " + str(label_target_indices[i]) + ")"))
else:
raise Exception("The current and target indices must have the same length")
return MR_list
[docs] @staticmethod
def scale_values_transform(x: np.array, scale_func) -> np.array:
"""
Scales all the values of the input
:param x: input data in the form of a numpy array
:param function scale_func: a function to be applied to all int values in the input
:return: the transformed data
"""
return np.vectorize(scale_func)(x)
[docs] def perform_MRs_tree(self, xs: np.array, ys: np.array, max_y: int) -> np.array:
"""
Performs the entire tree of MRs on the given data
:param xs: x numpy array
:param ys: y numpy array
:param max_y: the largest value y can be
:return: the transformed data and corresponding labels
"""
if len(xs) != len(ys):
raise Exception("xs and ys must have the same length")
if len(self.MR_tree) == 0:
return xs, ys
groups = Data.group_by_label(ys, max_y)
return Data.concat_lists([(xs, ys)] + [MR.perform_MRs(t, xs, ys, groups) for t in self.MR_tree])
[docs] @staticmethod
def perform_MRs_list(MR_list: tuple[Transform, list], xs: np.array, ys: np.array, max_y: int) -> np.array:
"""
Performs the entire tree of MRs on the given data
:param MR_list: an element of a list of MRs
:param xs: x numpy array
:param ys: y numpy array
:param max_y: the largest value y can be
:return: the transformed data and corresponding labels
"""
if len(xs) != len(ys):
raise Exception("xs and ys must have the same length")
if len(MR_list) == 0:
return xs, ys
groups = Data.group_by_label(ys, max_y)
return Data.concat_lists([(xs, ys)] + [MR.perform_MRs(MR_list, xs, ys, groups)])
[docs] @staticmethod
def perform_MRs(transform_branch: tuple[Transform, list], xs: np.array, ys: np.array,
groups: list[list[int]]) -> tuple[np.array, np.array]:
"""
Performs a single branch of the MRs tree
:param transform_branch: the branch of MRs in the form (current_transform, [following_transforms])
:param xs: x numpy array
:param ys: y numpy array
:param groups: indexed labels of the y data
:return: the transformed data and corresponding labels
"""
if len(xs) != len(ys):
raise Exception("xs and ys must have the same length")
transform, current_y, target_y = transform_branch[0].func, transform_branch[0].current, transform_branch[
0].target
next_transforms = transform_branch[1]
if current_y == -1:
xs = MR.perform_GMR(transform, xs)
else:
xs, none_count = MR.perform_DSMR(transform, xs, groups[current_y])
ys = np.full((len(groups[current_y]) - none_count,), target_y)
groups = Data.group_by_label(ys, len(groups))
if len(next_transforms) != 0:
xs, ys = Data.concat_lists([MR.perform_MRs(t, xs, ys, groups) for t in next_transforms])
return xs, ys
[docs] @staticmethod
def perform_GMR(transform, xs: np.array) -> np.array:
"""
Performs the GMR on all the x data
:param function transform: the transformation function
:param xs: numpy array of x data
:return: the transformed data (the shape is the same as the input)
"""
mr_xs = np.zeros(xs.shape)
for i in range(len(xs)):
mr_xs[i] = transform(xs[i])
return mr_xs
[docs] @staticmethod
def perform_DSMR(transform, xs: np.array, indices: list[int]) -> tuple[np.array, int]:
"""
Performs the DSMR on the x data given by indices
:param function transform: the transformation function
:param xs: numpy array of x data
:param indices: indexed labels of the y data this MR should be performed on
:return: the transformed data
"""
mr_xs = np.zeros(tuple([len(indices)] + list(xs.shape)[1:]))
j = 0
none_count = 0
for i in indices:
t = transform(xs[i])
if t is None:
none_count += 1
else:
mr_xs[j] = t
j += 1
new_mr_xs = np.zeros(tuple([len(mr_xs) - none_count] + list(mr_xs.shape)[1:]))
for i in range(len(new_mr_xs)):
new_mr_xs[i] = mr_xs[i]
return new_mr_xs, none_count