import time
import multiprocessing
import numpy as np

def determinant_sequential(matrix):
    n = len(matrix)
    mat = np.copy(matrix)

    for i in range(n):
        max_row = i
        for k in range(i + 1, n):
            if abs(mat[k][i]) > abs(mat[max_row][i]):
                max_row = k

        if abs(mat[max_row][i]) < 1e-9:
            return 0

        mat[[i, max_row]] = mat[[max_row, i]]

        for k in range(i + 1, n):
            factor = mat[k][i] / mat[i][i]
            for j in range(i, n):
                mat[k][j] -= factor * mat[i][j]

    det = 1
    for i in range(n):
        det *= mat[i][i]
    return det

def determinant_parallel_worker(matrix_part, row_indices):
    n_part = len(matrix_part)
    local_matrix = np.copy(matrix_part)
    local_row_indices = np.copy(row_indices)

    for i in range(n_part):
        max_row = i
        for k in range(i + 1, n_part):
            if abs(local_matrix[k][i]) > abs(local_matrix[max_row][i]):
                max_row = k

        if abs(local_matrix[max_row][i]) < 1e-9:
            return 0, local_row_indices

        local_matrix[[i, max_row]] = local_matrix[[max_row, i]]
        local_row_indices[[i, max_row]] = local_row_indices[[max_row, i]]

        for k in range(i + 1, n_part):
            factor = local_matrix[k][i] / local_matrix[i][i]
            for j in range(i, n_part):
                local_matrix[k][j] -= factor * local_matrix[i][j]

    det_part = 1
    for i in range(n_part):
        det_part *= local_matrix[i][i]
    return det_part, local_row_indices


def determinant_parallel(matrix, num_threads):
    n = len(matrix)
    if n == 1:
        return matrix[0][0]

    if num_threads > n:
        num_threads = n

    chunk_size = n // num_threads

    with multiprocessing.Pool(processes=num_threads) as pool:
        results = []
        row_indices = np.arange(n)
        for i in range(num_threads):
            start_row = i * chunk_size
            end_row = (i + 1) * chunk_size if i < num_threads - 1 else n
            res = pool.apply_async(determinant_parallel_worker,
                                   (matrix[start_row:end_row, :], row_indices[start_row:end_row]))
            results.append(res)

        partial_dets = []
        for res in results:
            det_part, row_indices_part = res.get()
            partial_dets.append(det_part)


        final_det = 1
        for det_part in partial_dets:
            final_det *= det_part

        return final_det



def calculate_determinant(matrix, num_threads):
    if num_threads == 1:
        return determinant_sequential(matrix)
    else:
        return determinant_parallel(matrix, num_threads)


if __name__ == '__main__':
    sizes = [100, 300, 500]
    num_threads_list = [1, 2, 4]

    for size in sizes:
        matrix = np.random.rand(size, size)
        for num_threads in num_threads_list:
            start_time = time.time()
            if num_threads == 1:
                calculation_method = "Последовательный"
            else:
                calculation_method = f"Параллельный ({num_threads} потоков)"
            det = calculate_determinant(matrix, num_threads)
            end_time = time.time()
            print(
                f"Размер матрицы: {size}x{size}, Метод вычисления: {calculation_method}, Время: {end_time - start_time:.4f} сек.")