class CombinationMatrixTest(unittest.TestCase): def setUp(self): self.data = [3, 3, 3, 4] self.t_value = 2 self.matrix = CombinationMatrix(self.data, self.t_value) self.excepted_hash_table = {(0, 1): CombinationRow(self.data, self.t_value, (0, 1)), (0, 2): CombinationRow(self.data, self.t_value, (0, 2)), (0, 3): CombinationRow(self.data, self.t_value, (0, 3)), (1, 2): CombinationRow(self.data, self.t_value, (1, 2)), (1, 3): CombinationRow(self.data, self.t_value, (1, 3)), (2, 3): CombinationRow(self.data, self.t_value, (2, 3))} def test_cover_solution_row(self): solution_row = [1, 0, 2, 3] excepted_uncovered = 57 excepted_covered_more_than_ones = 0 excepted_uncovered_row_size = 6 self.excepted_hash_table[0, 1].cover_cell((1, 0)) self.excepted_hash_table[0, 2].cover_cell((1, 2)) self.excepted_hash_table[0, 3].cover_cell((1, 3)) self.excepted_hash_table[1, 2].cover_cell((0, 2)) self.excepted_hash_table[1, 3].cover_cell((0, 3)) self.excepted_hash_table[2, 3].cover_cell((2, 3)) self.matrix.cover_solution_row(solution_row) self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key])) solution_row = [0, 0, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [0, 1, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [0, 2, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [1, 1, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [1, 2, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [2, 0, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [2, 1, 2, 3] self.matrix.cover_solution_row(solution_row) solution_row = [2, 2, 2, 3] self.matrix.cover_solution_row(solution_row) excepted_uncovered_row_size = 5 excepted_uncovered = 41 excepted_covered_more_than_ones = 13 self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") def test_cover_combination(self): solution_row = [1, 0, 2, 3] self.matrix.cover_combination(solution_row, (1, 0)) excepted_uncovered_row_size = 6 excepted_uncovered = 58 excepted_covered_more_than_ones = 0 self.excepted_hash_table[0, 1].cover_cell((1, 0)) self.excepted_hash_table[0, 2].cover_cell((1, 2)) self.excepted_hash_table[0, 3].cover_cell((1, 3)) self.excepted_hash_table[1, 2].cover_cell((0, 2)) self.excepted_hash_table[1, 3].cover_cell((0, 3)) self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key])) self.matrix.cover_combination(solution_row, (1, 0)) excepted_covered_more_than_ones = 5 self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") def test_uncover_solution_row(self): solution_row = [1, 0, 2, 3] self.matrix.cover_solution_row(solution_row) self.matrix.uncover_solution_row(solution_row) excepted_uncovered_row_size = 6 excepted_uncovered = 63 excepted_covered_more_than_ones = 0 self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key])) self.matrix.cover_solution_row(solution_row) self.matrix.cover_solution_row(solution_row) self.excepted_hash_table[0, 1].cover_cell((1, 0)) self.excepted_hash_table[0, 2].cover_cell((1, 2)) self.excepted_hash_table[0, 3].cover_cell((1, 3)) self.excepted_hash_table[1, 2].cover_cell((0, 2)) self.excepted_hash_table[1, 3].cover_cell((0, 3)) self.excepted_hash_table[2, 3].cover_cell((2, 3)) self.matrix.uncover_solution_row(solution_row) excepted_uncovered_row_size = 6 excepted_uncovered = 57 excepted_covered_more_than_ones = 0 self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key])) def test_uncover_combination(self): solution_row = [1, 0, 2, 3] self.matrix.cover_solution_row(solution_row) excepted_uncovered_row_size = 6 excepted_uncovered = 62 excepted_covered_more_than_ones = 0 self.excepted_hash_table[2, 3].cover_cell((2, 3)) self.matrix.uncover_combination(solution_row, (0, 1)) self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key])) def test_uncover(self): solution_row = [1, 0, 2, 3] self.matrix.cover_solution_row(solution_row) self.matrix.cover_solution_row(solution_row) excepted_uncovered_row_size = 6 excepted_uncovered = 63 excepted_covered_more_than_ones = 0 self.matrix.uncover() self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, "Total uovered_more_than_ones number is wrong.") self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), "Matrix has wrong uncovered row size") for key, value in self.matrix.hash_table.items(): with self.subTest(combination=key): self.assertTrue(combination_row_equals(value, self.excepted_hash_table[key]))
class Cit: def __init__(self, input_data, t_value, constraints): """ Creation of CombinationMatrix from user input :param input_data: parameters from user :param t_value: size of one combination :param constraints: constraints of combinations """ self.data = input_data self.t_value = t_value # CombinationMatrix creation self.combination_matrix = CombinationMatrix(input_data, t_value) # Creation of solver and simplification of constraints self.solver = Solver(input_data, constraints) # Combinations which do not match to the constraints are disabled self.solver.clean_hash_table(self.combination_matrix, t_value) self.final_matrix = [] self.__throbber = Throbber() def final_matrix_init(self): """ Creation of the first solution. This solution is the start of searching for the best solution :return: solution matrix (list(list)) """ self.final_matrix = [self.create_random_row_with_constraints()] self.combination_matrix.cover_solution_row(self.final_matrix[0]) while self.combination_matrix.total_uncovered != 0: if self.combination_matrix.total_uncovered < self.combination_matrix.total_covered_more_than_ones: new_row = self.compute_row() else: new_row = self.compute_row_using_hamming_distance() self.combination_matrix.cover_solution_row(new_row) self.final_matrix.append(new_row) return self.final_matrix def compute(self): """ Searching for the best solution. It creates one solution and from that, it tries to create smaller solution. This searching process is limited by ITERATIONS_SIZE. When ITERATIONS_SIZE is 0 the last found solution is the best solution. :return: The best solution """ self.final_matrix = self.final_matrix_init() matrix = [x[:] for x in self.final_matrix] iterations = ITERATIONS_SIZE step_size = 1 deleted_rows = [] while step_size != 0: for i in range(step_size): delete_row = matrix.pop(random.randint(0, len(matrix) - 1)) self.combination_matrix.uncover_solution_row(delete_row) deleted_rows.append(delete_row) LOG.debug("I'm trying solution with size %s and %s iterations", len(matrix), iterations) matrix, is_better_solution = self.find_better_solution(iterations, matrix) if is_better_solution: self.final_matrix = matrix[:] deleted_rows = [] step_size *= 2 LOG.debug("-----solution with size %s was found-----\n", len(matrix)) iterations = ITERATIONS_SIZE else: LOG.debug("-----solution with size %s was not found-----\n", len(matrix)) for i in range(step_size): self.combination_matrix.cover_solution_row(deleted_rows[i]) matrix.append(deleted_rows[i]) if step_size > 1: step_size = 1 else: step_size = 0 return self.final_matrix def find_better_solution(self, counter, matrix): """ Changing the matrix to cover all combinations :param counter: maximum number of changes in the matrix :param matrix: matrix to be changed :return: new matrix and is changes have been successful? """ while self.combination_matrix.total_uncovered != 0: LOG.debug(self.__throbber.render(), extra={"skip_newline": True}) solution, row_index, _ = self.use_random_algorithm(matrix) if len(solution) != 0: self.combination_matrix.uncover_solution_row(matrix[row_index]) self.combination_matrix.cover_solution_row(solution) matrix[row_index] = solution if counter == 0: return matrix, False counter -= 1 return matrix, True def use_random_algorithm(self, matrix): """ Applies one of these algorithms to the matrix. It chooses algorithm by random in proportion 1:1:8 :param matrix: matrix to be changed :return: new row of matrix, index of row inside matrix and parameters which has been changed """ switch = random.randint(0, 9) if switch == 0: solution, row_index, parameters = self.change_one_value(matrix) elif switch == 1: solution, row_index, parameters = self.change_one_column(matrix) else: solution, row_index, parameters = self.cover_missing_combination(matrix) return solution, row_index, parameters def compute_row(self): """ Computation of one row which covers most of combinations :return: new solution row """ is_valid_row = False while not is_valid_row: possible_parameters = list(self.combination_matrix.uncovered_rows) row = [-1] * len(self.data) while len(possible_parameters) != 0: # finding uncovered combination combination_parameters_index = random.randint(0, len(possible_parameters) - 1) combination_parameters = possible_parameters[combination_parameters_index] del possible_parameters[combination_parameters_index] combination_row = self.combination_matrix.get_row(combination_parameters) possible_combinations = list(combination_row.get_all_uncovered_combinations()) combination_index = random.randint(0, len(possible_combinations) - 1) combination = possible_combinations[combination_index] is_parameter_used = False # Are parameters already used in row? for i in combination_parameters: if row[i] != -1: is_parameter_used = True break if is_parameter_used: continue row_copy = row.copy() # Is combination matches the constraints? for index, parameter in enumerate(combination_parameters): row_copy[parameter] = combination[index] if self.combination_matrix.is_valid_solution(row_copy): row = row_copy # Filling in of free spaces inside the row for index, r in enumerate(row): if r == -1: is_valid = False while not is_valid: row[index] = random.randint(0, self.data[index] - 1) is_valid = self.combination_matrix.is_valid_solution(row) is_valid_row = self.combination_matrix.is_valid_solution(row) return row def cover_missing_combination(self, matrix): """ Randomly finds one missing combination. This combination puts into each row of the matrix. The row with the best coverage is the solution :param matrix: matrix to be changed :return: solution, index of solution inside matrix and parameters which has been changed """ parameters, combination = self.get_missing_combination_random() best_uncover = float("inf") best_solution = [] best_row_index = 0 for row_index in range(len(matrix)): solution = [x for x in matrix[row_index]] for index, item in enumerate(parameters): solution[item] = combination[index] if self.combination_matrix.is_valid_combination(solution, parameters): self.combination_matrix.uncover_combination(matrix[row_index], parameters) self.combination_matrix.cover_combination(solution, parameters) if self.combination_matrix.total_uncovered < best_uncover: best_uncover = self.combination_matrix.total_uncovered best_solution = solution best_row_index = row_index self.combination_matrix.uncover_combination(solution, parameters) self.combination_matrix.cover_combination(matrix[row_index], parameters) if best_uncover == 0: break return best_solution, best_row_index, parameters def get_missing_combination_random(self): """ Randomly finds one missing combination. :return: parameter of combination and values of combination """ possible_parameters = list(self.combination_matrix.uncovered_rows) combination_parameters_index = random.randint(0, len(possible_parameters) - 1) combination_parameters = possible_parameters[combination_parameters_index] combination_row = self.combination_matrix.get_row(combination_parameters) possible_combinations = list(combination_row.get_all_uncovered_combinations()) combination_index = random.randint(0, len(possible_combinations) - 1) combination = possible_combinations[combination_index] return combination_parameters, combination def change_one_column(self, matrix): """ Randomly choose one column of the matrix. In each cell of this column changes value. The row with the best coverage is the solution. :param matrix: matrix to be changed :return: solution, index of solution inside matrix and parameters which has been changed """ column_index = random.randint(0, len(self.data) - 1) best_uncover = float("inf") best_solution = [] best_row_index = 0 for row_index in range(len(matrix)): try: solution, row_index, parameters = self.change_one_value(matrix, row_index, column_index) except ValueError: continue self.combination_matrix.uncover_combination(matrix[row_index], parameters) self.combination_matrix.cover_combination(solution, parameters) if self.combination_matrix.total_uncovered < best_uncover: best_uncover = self.combination_matrix.total_uncovered best_solution = solution best_row_index = row_index self.combination_matrix.uncover_combination(solution, parameters) self.combination_matrix.cover_combination(matrix[row_index], parameters) if best_uncover == 0: break return best_solution, best_row_index, [column_index] def change_one_value(self, matrix, row_index=None, column_index=None): """ Change one cell inside the matrix :param matrix: matrix to be changed :param row_index: row inside matrix. If it's None it is chosen randomly :param column_index: column inside matrix. If it's None it is chosen randomly :return: solution, index of solution inside matrix and parameters which has been changed """ is_cell_chosen = True if row_index is None: is_cell_chosen = False row_index = random.randint(0, len(matrix) - 1) row = [x for x in matrix[row_index]] if column_index is None: is_cell_chosen = False column_index = random.randint(0, len(row) - 1) possible_numbers = list(range(0, row[column_index])) + list( range(row[column_index] + 1, self.data[column_index])) row[column_index] = random.choice(possible_numbers) while not self.combination_matrix.is_valid_combination(row, [column_index]): possible_numbers.remove(row[column_index]) if len(possible_numbers) == 0: if is_cell_chosen: raise ValueError("Selected cell can't be changed") column_index = random.randint(0, len(row) - 1) row_index = random.randint(0, len(matrix) - 1) row = [x for x in matrix[row_index]] possible_numbers = list(range(0, row[column_index])) + list( range(row[column_index] + 1, self.data[column_index])) row[column_index] = random.choice(possible_numbers) return row, row_index, [column_index] def compute_row_using_hamming_distance(self): """ :return: row with the biggest hamming distance from final matrix """ row_1 = self.create_random_row_with_constraints() row_2 = self.create_random_row_with_constraints() if self.compute_hamming_distance(row_1) >= self.compute_hamming_distance(row_2): return row_1 else: return row_2 def compute_hamming_distance(self, row): """ :return: hamming distance of row from final matrix """ distance = 0 for final_row in self.final_matrix: for index, cell in enumerate(final_row): if row[index] != cell: distance += 1 return distance def create_random_row_with_constraints(self): """ Create a new test-case random row, and the row meets the constraints. :return: new random row :rtype: list """ data_size = len(self.data) row = [-1]*data_size for parameter in random.sample(range(data_size), data_size): possible_values = self.solver.get_possible_values(row, parameter) value_choice = random.choice(possible_values) row[parameter] = value_choice return row