<reponame>DSE512/twelve<gh_stars>1-10
"""Question 2"""
import time
import numpy as np

from concurrent import futures


def timing(function):
    """Timing decorator

    Decorators allow us to wrap some
    `function` with added functionality;
    in this case, measuring how much time
    the function took to run.
    """
    def wrap(*args, **kwargs):
        start = time.perf_counter()
        ret = function(*args, **kwargs)
        end = time.perf_counter()
        print(f"Total time: {end-start} seconds.")
        return ret
    return wrap


def expand(step, num_steps):
    """A single step in the series expansion"""
    return 1 / (1 + ((step-0.5) / num_steps)**2)


def compute_series(num_steps=10):
    """Approximate pi by exapnding our series `num_steps`"""
    steps = []
    for step in range(num_steps):
        steps.append(
            expand(step, num_steps)
        )

    approx = 4 / num_steps * sum(steps)

    return num_steps, approx


@timing
def singlethread_series(expansions):
    for step in expansions:
        steps, approx = compute_series(step)
        print_difference(steps, approx)


@timing
def multithread_series(expansions):
    with futures.ThreadPoolExecutor() as executor:
        futs = executor.map(compute_series, expansions)

    for steps, approx in futs:
        print_difference(steps, approx)


def print_difference(steps, approx):
    print(
        f"Pi({steps}) = {approx:<20} | " \
        f"Pi - approximation = {np.pi-approx:.5}"
    )


def main():
    from viztracer import VizTracer

    expansions = [*range(10, 10_000, 50)]

    print(f"Single Thread Execution:")
    singlethread_series(expansions)

    tracer = VizTracer()
    tracer.start()

    print(f"Multi-thread Execution:")
    multithread_series(expansions)

    tracer.stop()
    tracer.save("multithread_results.html")


if __name__=="__main__":
    main()


"""Question 2"""
import time
import numpy as np

from concurrent import futures


def timing(function):
    """Timing decorator

    Decorators allow us to wrap some
    `function` with added functionality;
    in this case, measuring how much time
    the function took to run.
    """
    def wrap(*args, **kwargs):
        start = time.perf_counter()
        ret = function(*args, **kwargs)
        end = time.perf_counter()
        print(f"Total time: {end-start} seconds.")
        return ret
    return wrap


def expand(step, num_steps):
    """A single step in the series expansion"""
    return 1 / (1 + ((step-0.5) / num_steps)**2)


def compute_series(num_steps=10):
    """Approximate pi by exapnding our series `num_steps`"""
    steps = []
    for step in range(num_steps):
        steps.append(
            expand(step, num_steps)
        )

    approx = 4 / num_steps * sum(steps)

    return num_steps, approx


@timing
def singlethread_series(expansions):
    for step in expansions:
        steps, approx = compute_series(step)
        print_difference(steps, approx)


@timing
def multithread_series(expansions):
    with futures.ThreadPoolExecutor() as executor:
        futs = executor.map(compute_series, expansions)

    for steps, approx in futs:
        print_difference(steps, approx)


def print_difference(steps, approx):
    print(
        f"Pi({steps}) = {approx:<20} | " \
        f"Pi - approximation = {np.pi-approx:.5}"
    )


def main():
    from viztracer import VizTracer

    expansions = [*range(10, 10_000, 50)]

    print(f"Single Thread Execution:")
    singlethread_series(expansions)

    tracer = VizTracer()
    tracer.start()

    print(f"Multi-thread Execution:")
    multithread_series(expansions)

    tracer.stop()
    tracer.save("multithread_results.html")


if __name__=="__main__":
    main()
Question 2 Timing decorator      Decorators allow us to wrap some     `function` with added functionality;     in this case, measuring how much time     the function took to run. A single step in the series expansion Approximate pi by exapnding our series `num_steps`