import math
import numpy as np


############################################################
# binary representation of numbers
############################################################


n = 9 / 5
# print(n)
# print(f"{n:.30f}")
# print(f"{n:.60f}")
# print(np.binary_repr(np.float32(n).view(np.int32), width=32))


def sum_equal_parts(k):
    total = 0
    for _ in range(k):
        total += 1 / k
    return total


def test_sum_equal_parts():
    for k in range(2, 10):
        result = sum_equal_parts(k)
        print(k, f"{result:.60f}")


# test_sum_equal_parts()


############################################################
# exhaustive enumeration vs bisection search of continuous spaces
############################################################


def exhaustive_square_root(x, epsilon=1e-3):
    closest = 0
    min_error = float("inf")
    for guess in np.arange(0, x, epsilon):
        error = abs(guess ** 2 - x)
        if error < min_error:
            closest = guess
            min_error = error
    return closest


def test_exhaustive_square_root(epsilon=1e-3):
    queries = [2, 3, 4, 9, 25, 30.25, 50]
    for q in queries:
        print(exhaustive_square_root(q, epsilon))


# print()
# test_exhaustive_square_root()
# print()
# test_exhaustive_square_root(epsilon=1e-6)


def bisection_square_root(x, epsilon=1e-3):
    lower = 0
    upper = x
    mid = (lower + upper) / 2
    while upper - lower > epsilon:
        if mid ** 2 > x:
            upper = mid
        else:
            lower = mid
        mid = (lower + upper) / 2
    return mid


def test_bisection_square_root(precision=1e-3):
    queries = [2, 3, 4, 9, 25, 30.25, 50]
    for q in queries:
        result = bisection_square_root(q, precision)
        error = abs(result - math.sqrt(q))
        print(f"{q:5.2f}  {result:.20f}  {error:.20f}")


# print()
# test_bisection_square_root()
# print()
# test_bisection_square_root(precision=1e-6)


def bisection_square_root_query_precision(x, delta=1e-3):
    lower = 0
    upper = x
    mid = (lower + upper) / 2
    diff = mid ** 2 - x
    while abs(diff) > delta:
        if diff > 0:
            upper = mid
        else:
            lower = mid
        mid = (lower + upper) / 2
        diff = mid ** 2 - x
    return mid


# bisection_square_root = bisection_square_root_query_precision
# print()
# test_bisection_square_root()
# print()
# test_bisection_square_root(precision=1e-6)


# EXERCISE: When calculating a square root for a number x between 0 and
# 1, it is incorrect to use an initial upper bound of x. What would be
# an appropriate alternative?


# EXERCISE: Generalize the bisection method to work for any monotonic
# function, such as cube roots or logarithms.


# EXERCISE: The exhaustive method works by traversing the solution space
# with a step size of "epsilon". However, we could also base its exit
# condition on a "delta" precision against the input query. When would
# this strategy return an incorrect result?
# HINT: Consider the relationship between epsilon and delta when the
# query's magnitude is very large.
