#!/usr/bin/env python3

"""
This script runs Variational-Quantum-Eigensolver using Qiskit library

Example running it partially using CK infrastructure:
    ck virtual env  --tag_groups='compiler,python qiskit,lib vqe,utils vqe,hamiltonian deployed,ansatz deployed,optimizer' \
                    --shell_cmd="$HOME/CK/ck-qiskit/program/qiskit-vqe/qiskit_vqe_common.py --start_param_value=0.5"
"""

import os
import json
import time
import inspect

import numpy as np
from scipy import linalg as la

from qiskit import QuantumProgram, register, QISKitError
from qiskit.tools.apps.optimization import make_Hamiltonian, group_paulis
from qiskit.tools.qi.pauli import Pauli, label_to_pauli
from eval_hamiltonian import eval_hamiltonian

from vqe_utils import cmdline_parse_and_report, get_first_callable, NumpyEncoder

from vqe_hamiltonian import label_to_hamiltonian_coeff      # the file contents will be different depending on the plugin choice
import custom_ansatz                                        # the file contents will be different depending on the plugin choice

fun_evaluation_counter = 0    # global


def vqe_for_qiskit(sample_number, pauli_list, timeout_seconds, json_stream_file):

    def expectation_estimation(current_params, report):

        timestamp_before_ee = time.time()

        timestamp_before_q_run = timestamp_before_ee    # no point in taking consecutive timestamps

        ansatz_circuit  = ansatz_function(current_params)

        global fun_evaluation_counter

        # Trying to recover from a timed-out run assuming it to be a temporary glitch:
        #
        for attempt in range(1,8):
            try:
                complex_energy, q_run_seconds = eval_hamiltonian(Q_program, pauli_list_grouped, ansatz_circuit, sample_number, q_device_name, timeout=timeout_seconds)
                energy = complex_energy.real
                break
            except QISKitError as e:
                print("{}, trying again -- attempt number {}, timeout: {} seconds".format(e, attempt, timeout_seconds))

        if len(q_run_seconds)>0:                            # got the real measured q time
            total_q_run_seconds = sum( q_run_seconds )
        else:                                               # have to assume
            total_q_run_seconds = time.time() - timestamp_before_q_run
            q_run_seconds       = [ total_q_run_seconds ]

        q_runs              = len(q_run_seconds)
        total_q_run_shots   = sample_number * q_runs
        q_run_shots         = [sample_number] * q_runs

        report_this_iteration = {
            'total_q_seconds_per_c_iteration' : total_q_run_seconds,
            'seconds_per_individual_q_run' :    q_run_seconds,
            'total_q_shots_per_c_iteration' :   total_q_run_shots,
            'shots_per_individual_q_run' :      q_run_shots,
            'energy' : energy,
        }

        if report != 'TestMode':
            report['iterations'].append( report_this_iteration )
            report['total_q_seconds'] += report_this_iteration['total_q_seconds_per_c_iteration']  # total_q_time += total
            report['total_q_shots'] += report_this_iteration['total_q_shots_per_c_iteration']

            fun_evaluation_counter += 1

        report_this_iteration['total_seconds_per_c_iteration'] = time.time() - timestamp_before_ee

        print(report_this_iteration, "\n")
        json_stream_file.write( json.dumps(report_this_iteration, cls=NumpyEncoder)+"\n" )
        json_stream_file.flush()

        return energy

    # Initialise quantum program
    Q_program = QuantumProgram()

    # Groups a list of (coeff,Pauli) tuples into tensor product basis (tpb) sets
    pauli_list_grouped = group_paulis(pauli_list)


    report = { 'total_q_seconds': 0, 'total_q_shots':0, 'iterations' : [] }

    # Initial objective function value
    fun_initial = expectation_estimation(start_params, 'TestMode')
    print('Initial guess at start_params is: {:.4f}'.format(fun_initial))

    timestamp_before_optimizer = time.time()
    optimizer_output = minimizer_function(expectation_estimation, start_params, my_args=(report), my_options = minimizer_options)
    report['total_seconds'] = time.time() - timestamp_before_optimizer

    # Also generate and provide a validated function value at the optimal point
    fun_validated = expectation_estimation(optimizer_output['x'], 'TestMode')
    print('Validated value at solution is: {:.4f}'.format(fun_validated))

    # Exact (noiseless) calculation of the energy at the given point:
    complex_energy, _ = eval_hamiltonian(Q_program, pauli_list, ansatz_function(optimizer_output['x']), 1, 'local_statevector_simulator')
    optimizer_output['fun_exact'] = complex_energy.real

    optimizer_output['fun_validated'] = fun_validated

    print('Total Q seconds = %f' % report['total_q_seconds'])
    print('Total Q shots = %d' % report['total_q_shots'])
    print('Total seconds = %f' % report['total_seconds'])

    return (optimizer_output, report)


if __name__ == '__main__':

    start_params, sample_number, q_device_name, minimizer_method, minimizer_options, minimizer_function = cmdline_parse_and_report(
        num_params                  = custom_ansatz.num_params,
        q_device_name_default       = 'local_qasm_simulator',
        q_device_name_help          = "Real devices: 'ibmqx4' or 'ibmqx5'. Use 'ibmq_qasm_simulator' for remote simulator or 'local_qasm_simulator' for local",
        minimizer_options_default   = '{"maxfev":200, "xatol": 0.001, "fatol": 0.001}',
        start_param_value_default   = 0.0
        )
    # q_device_name = os.environ.get('VQE_QUANTUM_BACKEND', 'local_qasm_simulator') # try 'local_qasm_simulator', 'ibmq_qasm_simulator', 'ibmqx4', 'ibmqx5'

    try:
        import Qconfig
        register(Qconfig.APItoken, Qconfig.config["url"], verify=False,
                          hub=Qconfig.config["hub"],
                          group=Qconfig.config["group"],
                          project=Qconfig.config["project"])
    except:
        print("""
                WARNING: There's no connection with IBMQuantumExperience servers.
                cannot test I/O intesive tasks, will only test CPU intensive tasks
                running the jobs in the local simulator
                """)

    # Ignore warnings due to chopping of small imaginary part of the energy
    #import warnings
    #warnings.filterwarnings('ignore')

    # Load the Hamiltonian into Qiskit-friendly format:
    pauli_list = [ [label_to_hamiltonian_coeff[label], label_to_pauli(label)] for label in label_to_hamiltonian_coeff ]

    # Calculate Exact Energy classically, to compare with quantum solution:
    #
    H = make_Hamiltonian(pauli_list)
    classical_energy = np.amin(la.eigh(H)[0])
    print('The exact ground state energy (the smallest eigenvalue of the Hamiltonian) is: {:.4f}'.format(classical_energy))

    # Load the ansatz function from the plug-in
    ansatz_method   = get_first_callable( custom_ansatz )
    ansatz_function = getattr(custom_ansatz, ansatz_method)     # ansatz_method is a string/name, ansatz_function is an imported callable

    timeout_seconds = int( os.environ.get('VQE_QUANTUM_TIMEOUT', '120') )

    json_stream_file = open('vqe_stream.json', 'a')

    # ---------------------------------------- run VQE: --------------------------------------------------

    (vqe_output, report) = vqe_for_qiskit(sample_number, pauli_list, timeout_seconds, json_stream_file)

    # ---------------------------------------- store the results: ----------------------------------------

    json_stream_file.write( '# Experiment finished\n' )
    json_stream_file.close()

    minimizer_src   = inspect.getsource( minimizer_function )
    ansatz_src      = inspect.getsource( ansatz_function )

    vqe_input = {
        "q_device_name"     : q_device_name,
        "minimizer_method"  : minimizer_method,
        "minimizer_src"     : minimizer_src,
        "minimizer_options" : minimizer_options,
        "ansatz_method"     : ansatz_method,
        "ansatz_src"        : ansatz_src,
        "sample_number"     : sample_number,
        "classical_energy"  : classical_energy
        }

    output_dict     = { "vqe_input" : vqe_input, "vqe_output" : vqe_output, "report" : report }
    formatted_json  = json.dumps(output_dict, cls=NumpyEncoder, sort_keys = True, indent = 4)

#    print(formatted_json)

    with open('ibm_vqe_report.json', 'w') as json_file:
        json_file.write( formatted_json )
