def max_eof_p(sch, max_degree=None, debug=False, max_tests=None):
    """
    :param sch: Input scheme.
    :param max_degree: Max degree of result polinomial.
    :param debug: If True debug information will be printed due to process.
    :return:  Upper bound of probability of failure with up to max_degree-1 errors in scheme.
    """
    n = sch.inputs()
    m = sch.elements()

    nonerror = (0,) * m

    if max_degree is None:
        max_degree = m

    sch_process = d.make_process_func(sch)
    if debug:
        start = time.time()
        counter = 0
        while time.time() - start < 1:
            output_true = sch_process((0,) * n, (0,) * m)
            vec2num(output_true)
            vec2num(output_true)
            counter += 1
        process_time = (time.time() - start) / counter

        inputs = 2 ** n
        errors = sum(choose(m, degree) for degree in range(max_degree + 1))
        estimated_time = process_time * inputs * errors
        print("Estimated time eof_p: ", estimated_time)

    polinomial = [Fraction(0, 1)] * (m + 1)
    sch_process = d.make_process_func(sch)
    for input_values in product(range(2), repeat=n):
        if debug:
            print(input_values)
        for degree in range(max_degree):
            for error_comb in combinations(range(m), degree):
                error_vec = [0] * m
                for i in error_comb:
                    error_vec[i] = 1
                if sch_process(input_values, error_vec) != sch_process(input_values, nonerror):
                    for i in range(degree, m + 1):
                        polinomial[i] += choose(m - degree, i - degree) * (-1) ** (i - degree)
    polinomial = [coeff / 2 ** n for coeff in polinomial]
    for degree in range(max_degree, m + 1):
        for i in range(degree, m + 1):
            polinomial[i] += choose(m, degree) * choose(m - degree, i - degree) * (-1) ** (i - degree)

    return polinomial
def correlation_multiout(sch1, sch2):  # compares correlation between two circuits (0% - 100%)
    if sch1.inputs() != sch2.inputs():
        return False
    if sch1.outputs() != sch2.outputs():
        return False
    correl = 0
    capacity = min(32, 2 ** sch1.inputs())
    mask = 2 ** capacity - 1
    sch1_func = d.make_process_func(sch1, capacity=capacity)
    sch2_func = d.make_process_func(sch2, capacity=capacity)
    n = sch1.inputs()
    for i in inputs_combinations(sch1.inputs(), capacity=capacity):
        output = (mask ^ out1 ^ out2 for out1, out2 in zip(sch1_func(i), sch2_func(i)))
        correl += sum(map(ones, output))
    correl /= 2 ** n * sch1.outputs()
    return correl
def max_transition_table_p(sch, indexes=None, max_degree=None, debug=False):
    """
    :param sch: Input scheme.
    :param indexes: Indexes of scheme outputs.
    :return: Table of probabilities (k coefficient in k*p + O(p)) of transition form one output combination to another.
    """
    n = sch.inputs()
    l = sch.elements()
    m = sch.outputs()

    if indexes is None:
        ttable_size = 2 ** sch.outputs()
        indexes = range(m)
    else:
        ttable_size = 2 ** len(indexes)

    if max_degree is None or max_degree > l:
        max_degree = l

    ttable = [[[0 for i in range(l + 1)] for x in range(ttable_size)] for x in range(ttable_size)]
    nonerror = [0] * l

    tests = 2 ** n
    errors = 2 ** l

    sch_process = d.make_process_func(sch)

    start = time.time()
    counter = 0
    while time.time() - start < 5:
        output_true = sch_process((0,) * n, (0,) * l)
        vec2num(output_true)
        vec2num(output_true)
        counter += 1
    process_time = (time.time() - start) / counter

    inputs = 2 ** n
    errors = sum(choose(l, degree) for degree in range(max_degree + 1))
    estimated_time = process_time * inputs * errors
    print("Estimated time: ", estimated_time)

    for input_values in product(range(2), repeat=n):
        if debug:
            print(input_values)
        output_true = sch_process(input_values, nonerror)
        if len(indexes) < m:
            output_true = [output_true[index] for index in indexes]
        for degree in range(max_degree):
            for error_comb in combinations(range(l), degree):
                error_vec = [0] * l
                for i in error_comb:
                    error_vec[i] = 1
                if len(indexes) < m:
                    output_error = [sch_process(input_values, error_vec)[index] for index in indexes]
                else:
                    output_error = sch_process(input_values, error_vec)
                true_index = vec2num(output_true)
                error_index = vec2num(output_error)
                for i in range(degree, max_degree + 1):
                    ttable[true_index][error_index][i] += (
                        choose(l - degree, i - degree) * (-1) ** (i - degree) / (2 ** n)
                    )
        for degree in range(max_degree, l + 1):
            pass
    return ttable
def eof_p_interval(sch, max_degree=None, debug=False, max_tests=None):
    """
    :param sch: Input scheme.
    :param max_degree: Max degree of result polinomial.
    :param debug: If True debug information will be printed due to process.
    :param capacity: Number of bits in used numbers.
    :param max_tests: Maximum number of tests to process.
    :return: First max_degree+1 members of polinomial EOF(p) for input scheme.
    """

    tests_performed = 0

    n = sch.inputs()
    m = sch.elements()

    sch_process = d.make_process_func(sch)

    nonerror = (0,) * m

    min_polynomial = [Fraction(0, 1)] * (m + 1)
    max_polynomial = [Fraction(1, 1)] + [Fraction(0, 1)] * m

    if max_degree is None:
        max_degree = m

    if max_tests is None or max_tests >= 2 ** (n + m):
        max_tests = 2 ** (n + m)

    def inputs_generator(rand=False):
        if rand:
            return product(*[random.choice(((0, 1), (1, 0))) for _ in range(n)])
        else:
            return product((0, 1), repeat=n)

    def make_errors_vector(indexes):
        errors_vector = [0] * m
        for index in indexes:
            errors_vector[index] = 1
        return tuple(errors_vector)

    def errors_generator(degree, rand=False):
        if rand:
            return (
                make_errors_vector(indexes=combination)
                for combination in combinations(random.sample(list(range(m)), m), r=degree)
            )
        else:
            return (make_errors_vector(indexes=combination) for combination in combinations(list(range(m)), r=degree))

    def update_polynomials(successes, fails, degree):
        for i in range(m + 1 - degree):
            min_polynomial[i + degree] += Fraction((-1) ** i * choose(m - degree, i) * fails, 2 ** n)
            max_polynomial[i + degree] -= Fraction((-1) ** i * choose(m - degree, i) * successes, 2 ** n)

    for degree in range(max_degree + 1):
        tests_remained = max_tests - tests_performed
        if debug:
            print(max_tests, tests_remained)
        rand = tests_remained < choose(m, degree) * 2 ** n
        errors_number = choose(m, degree)
        if rand:
            inputs_number = max(1, min(int(tests_remained / errors_number), 2 ** n))
        else:
            inputs_number = 2 ** n

        total_fails = 0
        total_successes = 0

        for errors in errors_generator(degree, rand):
            if not tests_remained:
                return min_polynomial, max_polynomial
            inputs_number = min(inputs_number, tests_remained)
            fails = sum(
                sch_process(inputs, errors) != sch_process(inputs)
                for inputs in islice(inputs_generator(rand), inputs_number)
            )
            successes = inputs_number - fails
            total_fails += fails
            total_successes += successes
            tests_remained -= inputs_number
            tests_performed += inputs_number

        update_polynomials(total_successes, total_fails, degree)

    return min_polynomial, max_polynomial
def eof_p_opt(sch, max_degree=None, debug=False, capacity=None):
    """
    :param sch: Input scheme.
    :param max_degree: Max degree of result polinomial.
    :param debug: If True debug information will be printed due to process.
    :return: First max_degree+1 members of polinomial EOF(p) for input scheme.
    """
    n = sch.inputs()
    m = sch.elements()

    if capacity is None:
        capacity = min([2 ** sch.inputs(), 32])

    nonerror = (0,) * m

    if max_degree is None:
        max_degree = m

    sch_process = d.make_process_func(sch, capacity=capacity)

    if debug:
        start = time.time()
        counter = 0
        while time.time() - start < 1:
            output_true = sch_process((0,) * n, (0,) * m)
            vec2num(output_true)
            vec2num(output_true)
            counter += 1
        process_time = (time.time() - start) / counter

        inputs = 2 ** n / capacity
        errors = sum(choose(m, degree) for degree in range(max_degree + 1))
        estimated_time = process_time * inputs * errors
        print("Estimated time eof_p: ", estimated_time)

    poly = Polynomial([Fraction(0, 1)])
    input_prob = Fraction(1, 2 ** n)
    p = Polynomial([Fraction(0, 1), Fraction(1, 1)])
    sum_poly = Polynomial([Fraction(0, 1)])
    for input_values in inputs_combinations(n, capacity=capacity):
        output_true = sch_process(input_values, nonerror)
        if debug:
            print(input_values)
        for degree in range(max_degree + 1):
            error_prob = Polynomial(polypow(p.coef, degree, maxpower=100)) * Polynomial(
                polypow((1 - p).coef, m - degree, maxpower=100)
            )
            # print(error_prob.coef)
            errors_number = 0
            if debug:
                print(len(list(combinations(range(m), r=degree))))
            for error_comb in combinations(range(m), r=degree):
                error_vec = [0] * m
                for i in error_comb:
                    error_vec[i] = 2 ** capacity - 1
                output_error = sch_process(input_values, error_vec)
                errors = [i ^ j for i, j in zip(output_true, output_error)]
                errors_number += ones(reduce(or_, errors))
            if debug:
                print(errors_number)
            if errors_number:
                poly += errors_number * input_prob * error_prob
    result = list(poly.coef)
    # removing trailing zeros
    while not result[-1]:
        result.pop()
    return result