%matplotlib inline
from qiskit import QuantumCircuit, transpile, QuantumRegister, ClassicalRegister
from qiskit.visualization import plot_histogram
from qiskit.quantum_info import Statevector

from qiskit_aer import AerSimulator
from qiskit_ibm_provider import IBMProvider

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from dotenv import load_dotenv
from tqdm.notebook import tqdm

import functools
import os
from time import time
from collections import defaultdict 

from utils import fibonacci, fibonacci_no_cache, get_image_path

ONE_KM_TO_MILES=0.621371

@functools.lru_cache(maxsize=1000)
def km_to_miles(km, last=0, n=0):
    fib_value = fibonacci(n)

    if(fib_value == km):
        return last

    if(fib_value > km*ONE_KM_TO_MILES):
        raise Exception(f"{km} Not in the sequence")

    return km_to_miles(km, fib_value, n+1)

km_to_miles(21)

@functools.lru_cache(maxsize=1000)
def miles_to_km(miles, last=0, n=0):
    fib_value = fibonacci(n)

    if(last == miles):
        return fib_value

    if(fib_value > miles/ONE_KM_TO_MILES):
        raise Exception(f"{miles} Not in the sequence")

    return miles_to_km(miles, fib_value, n+1)

miles_to_km(13)

km_miles = lambda km : km*ONE_KM_TO_MILES
miles_km = lambda miles: miles/ONE_KM_TO_MILES

km_miles(12)

km_miles(24)

BASE_NUMBERS = (1, 2, 3, 5, 8, 13, 21)

def breakdown(n, base_numbers=BASE_NUMBERS):
    used_numbers = defaultdict(int)
    for base_number in reversed(base_numbers):
        while(base_number <= n):
            used_numbers[base_number] += 1
            n -= base_number
    return dict(used_numbers)

breakdown(10)

def get_fib_pos(n, base_numbers=BASE_NUMBERS):
    return base_numbers.index(n)

def get_relative_miles_km(n):
    numbers = list(BASE_NUMBERS)+[34]
    pos = get_fib_pos(n)
    return numbers[pos+1]

def get_relative_km_miles(n):
    numbers = [1]+list(BASE_NUMBERS)
    pos = get_fib_pos(n)
    return numbers[pos]

get_relative_miles_km(8)

get_relative_km_miles(8)

def new_miles_to_km(n):
    result = 0
    factors = breakdown(n)
    for (number, total) in factors.items():
        relative = get_relative_miles_km(number)
        result += (relative*total)
    return result

new_miles_to_km(15)

def new_km_to_miles(n):
    result = 0
    factors = breakdown(n)
    print(factors)
    for (number, total) in factors.items():
        relative = get_relative_km_miles(number)
        result += (relative*total)
    return result

new_km_to_miles(12)

PI = np.pi

class Oracle(QuantumCircuit):
    def __init__(self, n_qubits):
        assert n_qubits >= 2, "Invalid number of qubits!"
        self.n_qubits = n_qubits
        super().__init__(n_qubits, name="oracle")
        self.generate()
        
    def generate(self):
        self.ry(PI/2, range(self.n_qubits))
        for i in range(self.n_qubits-1):
            self.cry(-PI/2, i, i+1)

    def simulate(self, backend=AerSimulator(), shots=1000):
        qc_copy = self.copy()
        qc_copy.measure_all()
        transpiled_qc = transpile(qc_copy, backend)
        return backend.run(transpiled_qc).result().get_counts()

oracle = Oracle(2)
display(oracle.draw('mpl'))

class KM_to_Miles(object):
    def __init__(self, iterations=3, base_oracle=Oracle):
        self.iterations = iterations
        self.total_qubits = 2
        self.oracle = base_oracle
        self.qc = self.oracle(2)
        self.results = pd.DataFrame(columns=['n_qubits', 'result'])
        
    def run(self, backend=AerSimulator(), shots=1000, debug=True):
        for i in tqdm(range(self.iterations)):
            results = self.qc.simulate(backend, shots)
            fibonacci_number = len(results)

            if(debug):
                print('-'*20)
                print(f'i: {i}')
                print(f'n: {self.total_qubits}')
                print(f'F({i}): {fibonacci_number}') 
            
            self.results = pd.concat([self.results, pd.DataFrame({'n_qubits':self.total_qubits, 'result':fibonacci_number}, columns=['n_qubits', 'result'], index=[iter])], ignore_index=True)
            
            self.total_qubits+=1
            self.qc = self.oracle(self.total_qubits)

converter = KM_to_Miles(10)
converter.run(debug=False)

quantum_results = converter.results
quantum_results

def generate_miles_km_table(iter=3):
    return pd.DataFrame([(fibonacci(i), fibonacci(i+1)) for i in range(iter)], columns=['miles', 'km'])

miles_km_data = generate_miles_km_table(12).loc[2:].reset_index().drop(columns=['index'])
miles_km_data

miles_km_data.shape, quantum_results.shape

def get_all_data(iter, converter, shots=1000):
    miles_km = generate_miles_km_table(iter+2).loc[2:].reset_index().drop(columns=['index'])
    
    converter_obj = KM_to_Miles(iter)
    converter_obj.run(shots=shots, debug=False)
    
    all_data = pd.concat([miles_km, converter_obj.results], axis=1)
    return all_data
    
result_data = get_all_data(24, converter, shots=100000)
result_data.tail()

fig, ((ax1,ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 10))

first_batch = result_data.loc[:8]
ax1.plot('miles', 'km', data=first_batch)
ax1.plot('miles', 'result', data=first_batch)
ax1.set_title('[0-7]')
ax1.set(xlabel="miles", ylabel="km")

second_batch = result_data.loc[8:16]
ax2.plot('miles', 'km', data=second_batch)
ax2.plot('miles', 'result', data=second_batch)
ax2.set_title('[8-15]')
ax2.set(xlabel="miles", ylabel="km")

third_batch = result_data.loc[16:]
ax3.plot('miles', 'km', data=third_batch)
ax3.plot('miles', 'result', data=third_batch)
ax3.set_title('[16-23]')
ax3.set(xlabel="miles", ylabel="km")

ax4.set_visible(False)

fig.legend(["classical", "quantum"], bbox_to_anchor=(0.75,0.4), fontsize=20)
plt.savefig(get_image_path("miles_to_km_defiance.png"))
plt.show()

QUANTUM_BASE_NUMBERS = (3, 5, 8, 13, 21)

def get_quantum_fib_numbers(inp_value, base_numbers=QUANTUM_BASE_NUMBERS):

    breakdown_values = breakdown(inp_value, base_numbers=base_numbers)
    remain = inp_value - sum(breakdown_values.keys()) 
    remain_values = {}

    if(remain > 0):
        REMAIN_BASE_NUMBERS = (1, 2)
        remain_values = breakdown(remain, base_numbers=REMAIN_BASE_NUMBERS)
        
    return breakdown_values, remain_values
    
values, remain = get_quantum_fib_numbers(100)
values

total_n_qubits = [ get_fib_pos(i, base_numbers=QUANTUM_BASE_NUMBERS)+2 for i in values ]
n_qubits = max(total_n_qubits)
measures = len(total_n_qubits)
total_n_qubits, n_qubits, measures

qubits = QuantumRegister(n_qubits, name="q")
measurements = [ClassicalRegister(cbits, name=index) for (index, cbits) in enumerate(total_n_qubits)]
qc = QuantumCircuit(qubits, *measurements)

for n, measurement in zip(total_n_qubits, measurements):
    qc.append(Oracle(n), range(n))
    qc.measure(range(n), measurement)
    qc.reset(range(n))
    qc.barrier()

qc.draw('mpl', filename=get_image_path("number_breakdown_circuit.png"))

def get_counts(qc, sim=AerSimulator()):
    return sim.run(transpile(qc, sim)).result().get_counts()
result = get_counts(qc)
plot_histogram(result)

bit_string = result.keys()
total = result.values()
data = {"bit_string": bit_string, 'total':total}

df = pd.DataFrame(data, columns=['bit_string', 'total'])
df.head()

df[['bit_string0', 'bit_string1', 'bit_string2']] = df['bit_string'].str.split(' ', n=2, expand=True)
df.head()

len(df['bit_string0'].unique()), len(df['bit_string1'].unique()), len(df['bit_string2'].unique())

def get_times(func, values_range):    
    results = []
    
    for i in values_range:
        start = time()
        func(i)
        results.append(time() - start)

    return results

def quantum_run(n):
    qc = Oracle(n)
    qc.simulate()

values_range = range(2, 25)

classical_no_cache_results = pd.Series(get_times(fibonacci_no_cache, values_range), dtype=np.float64)
classical_cache_results = pd.Series(get_times(fibonacci, values_range), dtype=np.float64)
quantum_local = pd.Series(get_times(quantum_run, values_range), dtype=np.float64)

times_data = pd.DataFrame({
    "n": pd.Series(list(values_range), dtype=np.int8),
    "classical_nc": classical_no_cache_results,
    "classical_c": classical_cache_results,
    "quantum_local": quantum_local
})

times_data.head()

load_dotenv()
IBMProvider.save_account(token=os.getenv("IBM_TOKEN"), overwrite=True)

provider = IBMProvider()
backends = provider.backends(min_num_qubits=25, operational=True, simulator=False)

min_pending_jobs = 1000000000
selected_backend = ''

for backend in backends:
    backend_name = backend.name
    pending_jobs = backend.status().pending_jobs

    if(pending_jobs < min_pending_jobs):
        min_pending_jobs = pending_jobs
        selected_backend = backend_name
    
    print(f'name: {backend_name}')
    print(f'qubits: {backend.num_qubits}')
    print(f'max_shots: {backend.max_shots}')
    print(f'jobs: {pending_jobs}')
    print(f'max_circuits: {backend.max_circuits}', end='\n\n')

backend = provider.get_backend(selected_backend)
circuits = [transpile(Oracle(n), backend) for n in values_range ]
ids = []

for i,circuit in enumerate(circuits):
    status = None
    
    job = backend.run(circuit, shots=1000)
    id = job.job_id()
    
    print(f'circuit: {i+1}')
    print(f'job: {id}', end='\n\n')
    
    ids.append(id)
    
    while status != 'DONE':
        status = job.status().name


results = [provider.retrieve_job(job_id).result()._metadata['time_taken'] for job_id in ids]
quantum_ibm = pd.Series(results, dtype=np.float64)

times_data['quantum_ibm'] = quantum_ibm
times_data.head()

index = times_data.index.tolist()
plt.plot(index, "classical_nc", data=times_data)
plt.plot(index, "classical_c", data=times_data)
plt.plot(index, "quantum_local", data=times_data)
plt.plot(index, "quantum_ibm", data=times_data)

plt.legend()

plt.savefig(get_image_path("comparison_miles_to_km.png"))

plt.show()