diff --git a/.gitignore b/.gitignore index 1027fc4ff3..ec085d5b59 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.py[cod] *$py.class +src/qibo/tomography_RGD/testData/ # C extensions *.so diff --git a/src/qibo/tomography_RGD/BasicTools.py b/src/qibo/tomography_RGD/BasicTools.py new file mode 100755 index 0000000000..e76a5b2bdd --- /dev/null +++ b/src/qibo/tomography_RGD/BasicTools.py @@ -0,0 +1,57 @@ +# +# some tools +# 1. generate all labels +# 2. plot the results +# + +# --------------------------------- # +# to generate all symbols # +# --------------------------------- # +from itertools import product + +import matplotlib.pyplot as plt + + +def Generate_All_labels(nqubits, symbols=["I", "X", "Y", "Z"]): + all_labels = ["".join(elem) for elem in product(symbols, repeat=nqubits)] + return all_labels + + +def Plt_Err_Time(worker): + """plot the Error w.r.t. optimization run time + + Args: + worker (class): the optimization class instance + """ + + Target_Err_Xk = worker.Target_Err_Xk + step_Time = worker.step_Time + + RunT = [] + Ttot = 0 + for ti in range(-1, len(step_Time) - 1): + Each_Step = step_Time[ti] + Ttot += Each_Step + RunT.append(Ttot) + + mk_list = ["+", "^", "o", "x", ">", "<", 2, 3] + ln_list = ["-", "-.", "--", "--", ":", "-"] + + fig, axNow = plt.subplots(1, 1, figsize=(8, 6)) + + info = "{} qubits with sampling {} labels".format(worker.n, worker.num_labels) + axNow.plot( + RunT, + Target_Err_Xk, + marker=mk_list[0], + linestyle=ln_list[0], + label="{}".format(info), + ) + + axNow.set_xlabel(" Run time (sec)", fontsize=14) + axNow.set_ylabel(r"$\left\Vert X_k -\rho \right\Vert_F$", fontsize=14) + + axNow.set_title("Error w.r.t. run time", y=1.0, fontsize=14) + + plt.legend(loc="upper left") + plt.show() diff --git a/src/qibo/tomography_RGD/Run_Tomo.py b/src/qibo/tomography_RGD/Run_Tomo.py new file mode 100755 index 0000000000..ac20b65b50 --- /dev/null +++ b/src/qibo/tomography_RGD/Run_Tomo.py @@ -0,0 +1,296 @@ +# +# prepare the state, do the measurements +# and directly do the tomography +# +# + +import measurements +import methodsMiFGD_core +import methodsRGD_core +import numpy as np +import projectors +import qutip as qu +from BasicTools import Generate_All_labels, Plt_Err_Time +from measurements import Measurement + +# from states import GHZState, HadamardState, RandomState +from qibo_states import GHZState, HadamardState, RandomState + +from qibo.models.encodings import ghz_state +from qibo.quantum_info import random_density_matrix + + +def effective_parity(key, label): + """Calculates the effective number of '1' in the given key + + Args: + key (str): the measurement outcome in the form of 0 or 1 + (eg) '101' or '110' for the 3 qubit measurement + label (str): the label for Pauli measurement + (eg) 'XIZ', 'YYI', 'XZY' for the 3 qubit measurement + Returns: + int: the number of effective '1' in the key + """ + indices = [i for i, symbol in enumerate(label) if symbol == "I"] + digit_list = list(key) + for i in indices: + digit_list[i] = "0" + effective_key = "".join(digit_list) + + return effective_key.count("1") + + +def count_dict_2_PaulCoef(label, count_dict): + """to convert the shot measurement result for the given Pauli label + into the coefficient of the label in the Pauli operator basis + + Args: + label (str): the label for Pauli measurement + (eg) 'XIYZ', 'XYYI', 'ZXZY' for the 4 qubit measurement + count_dict (dict): the shot measurement result for the label + (eg) {'0011': 9, '0100': 7, '1001': 8, '0101': 11, '1101': 3, + '0001': 9, '1000': 9, '0000': 12, '1110': 19, '1111': 4, + '0111': 1, '1100': 2, '1010': 5, '0110': 1} + + Returns: + (float): the coefficient in the Pauli operator basis corresponding to the label + """ + num_shots = sum(count_dict.values()) + + freq = {k: (v) / (num_shots) for k, v in count_dict.items()} + parity_freq = {k: (-1) ** effective_parity(k, label) * v for k, v in freq.items()} + coef = sum(parity_freq.values()) + # data2 = {label: coef} + + return coef + + +def Test_measurement(labels, data_dict_list): + + num_labels = len(labels) + + # + # shot measurement results --> coefficient for each Pauli operator + # + + data_dict = {} + for ii in range(num_labels): + label = data_dict_list[ii]["label"] + data_dict[label] = data_dict_list[ii]["count_dict"] + + measurement_list = measurements.MeasurementStore.calc_measurement_list( + labels, data_dict + ) + + # + # = measurements.MeasurementStore.calc_measurement_list + # + count_dict_list = [data_dict[label] for label in labels] # in the order of labels + measurement_list2 = measurements.measurement_list_calc(labels, count_dict_list) + print(np.array(measurement_list2) - np.array(measurement_list)) + + # + # how measurements.measurement_list_calc works + # + measurement_object_list = [ + Measurement(label, count_dict) + for (label, count_dict) in zip(*[labels, count_dict_list]) + ] + + parity_flavor = "effective" + beta = None + measurement_list3 = [ + measurement_object.get_pauli_correlation_measurement(beta, parity_flavor)[label] + for (label, measurement_object) in zip(*[labels, measurement_object_list]) + ] + print(np.array(measurement_list3) - np.array(measurement_list)) + + return count_dict_list, measurement_list + + +if __name__ == "__main__": + + ############################################################ + ### Example of creating and running an experiment + ############################################################ + + # n = 3; labels = projectors.generate_random_label_list(50, n) + n = 4 + labels = projectors.generate_random_label_list(120, n) + + # labels = ['YXY', 'IXX', 'ZYI', 'XXX', 'YZZ'] + # labels = ['YZYX', 'ZZIX', 'XXIZ', 'XZIY', 'YXYI', 'ZYYX', 'YXXX', 'IIYY', 'ZIXZ', 'IXXI', 'YZXI', 'ZZYI', 'YZXY', 'XYZI', 'XZXI', 'XZYX', 'YIXI', 'IZYY', 'ZIZX', 'YXXY'] + # labels = ['IIIX', 'IYIY', 'YYXI', 'ZZYY', 'ZYIX', 'XIII', 'XXZI', 'YXZI', 'IZXX', 'YYIZ', 'XXIY', 'XXZY', 'ZZIY', 'YIYX', 'YYZZ', 'YZXZ', 'YZYZ', 'ZXYY', 'IXIZ', 'XZII'] + # labels = Generate_All_labels(n) + + num_labels = len(labels) + + circuit_Choice = 1 + if circuit_Choice == 1: # generate from circuit + Nr = 1 + + state = GHZState(n) + # state = HadamardState(n) + # state = RandomState(n) + + stateGHZ = ghz_state(n) + target_state_GHZ = stateGHZ.execute().state() + + target_density_matrix = state.get_state_matrix() + target_state = state.get_state_vector() + # print(state.get_state_vector()) + + # + # DO the shot measurement + # + + state.create_circuit() + data_dict_list = state.execute_measurement_circuits(labels) + # print(data_dict_list) + + count_dict_list, measurement_list = Test_measurement(labels, data_dict_list) + + # + # count_dict_list --> measurement_list directly + # + + measurement_list4 = [] + + for label, count_dict in zip( + *[labels, count_dict_list] + ): # count_dict_list in the order of labels + + coef = count_dict_2_PaulCoef(label, count_dict) + + measurement_list4.append(coef) + + print(np.allclose(np.array(measurement_list4), np.array(measurement_list))) + raise + + elif circuit_Choice == 2: # directly generate density matrix via qutip + Nr = 1 + rho = qu.rand_dm_ginibre(2**n, dims=[[2] * n, [2] * n], rank=Nr) + target_density_matrix = rho.full() + + elif circuit_Choice == 3: # directly generate density matrix via qibo + Nr = 3 + target_density_matrix = random_density_matrix(2**n, Nr) + + # ---------------------------------------------------------------- # + # construct Pauli matrix Projectors # + # ---------------------------------------------------------------- # + + projector_store_path = "./testData/qiskit" + # projector_store_path = './testData/qibo' + projector_store = projectors.ProjectorStore(labels) + + ## store method (1) + # projector_store.populate(projector_store_path) + # projector_dict = projectors.ProjectorStore.load(projector_store_path, labels) + + ## store method (2) + num_cpus, saveP_bulk, Partition_Pj = projector_store.mpPool_map( + projector_store_path + ) + projector_dict = projectors.ProjectorStore.load_PoolMap( + projector_store_path, labels + ) + + # ---------------------------------------------------------------- # + # calculate exact coefficient for each Pauli operator # + # ---------------------------------------------------------------- # + + projector_list = [projector_dict[label] for label in labels] + yProj_Exact = methodsRGD_core.Amea( + projector_list, target_density_matrix, num_labels, 1 + ) # argv[-1] = coef + + if circuit_Choice == 1: # generated from circuit + # + # comparison: manual check the result of shot measurements + # + ml = np.array(measurement_list, dtype=float) + ind = np.where(np.abs(yProj_Exact) > 0.5) + + print(yProj_Exact[ind]) + print(ml[ind]) + elif circuit_Choice == 2 or circuit_Choice == 3: # directly generate density matrix + measurement_list = yProj_Exact + target_state = None + + # + # system parameters + # + params_dict = { + "Nr": Nr, + "target_DM": target_density_matrix, + "labels": labels, + "measurement_list": measurement_list, + "projector_list": projector_list, + "num_iterations": 150, + "convergence_check_period": 1, + } + # params_dict['target_state'] = target_state + + # ----------------------------------------------------------------- # + # do the tomography optimization # + # ----------------------------------------------------------------- # + + # + # MiFGD numerical parameters + # + + # Call_MiFGD = 1 # = 1: call the MiFGD optimization to calculate + # muList = [4.5e-5] + + InitX_MiFGD = 1 # 0: random start, 1: MiFGD specified init + mu = 4.5e-5 + eta = 0.01 + Option = 2 + + # Num_mu = 1 # number of mu for running MiFGD + # pm_MiFGD = [Call_MiFGD, InitX_MiFGD, muList, Num_mu] + # Call_MiFGD, InitX_MiFGD, muList, Num_mu = pm_MiFGD + + params_MiFGD = {"mu": mu, "eta": eta, "Option": Option} + # params_dict = {**params_dict, **params_MiFGD} + + # Rpm_MiFGD = [InitX_MiFGD, mu, eta, Option] + # Frec_MiFGD, wc, RunTime = Run_MiFGD(params_dict, Rpm_MiFGD) + + worker2 = methodsMiFGD_core.BasicWorker(params_dict, params_MiFGD) + worker2.compute(InitX_MiFGD) + + # + # RGD numerical parameters + # + + print("\n +++++++++++++++++ do the RGD tomography +++++++++++++++++\n") + + # Call_RGD = 1 # = 1: call the RGD optimization to calculate + + Md_tr = 0 # Method if including trace = 1 + Md_alp = 0 # method for scaling alpha + Md_sig = 0 # method for scaling singular value + Ch_svd = ( + -1 + ) # choice for initial SVD (0: LA.svd, 1: svds; 2: power_Largest_EigV, -1: rSVD) + InitX_RGD = 1 + # method of choosing initial X0 + + # Rpm = [InitX_RGD, Md_tr, Md_alp, Md_sig, Ch_svd] + # pm_RGD = [Call_RGD, Rpm] + # Call_RGD, Rpm = pm_RGD + + # InitX_RGD, Md_tr, Md_alp, Md_sig, Ch_svd = Rpm + # InitX_RGD = 1 + # Rpm = InitX_RGD, Md_tr, Md_alp, Md_sig, Ch_svd + + # exec(open('RGD_optRun.py').read()) + # Frec_RGD, wc, RunTime = Run_RGD(params_dict, Rpm) + + worker = methodsRGD_core.BasicWorkerRGD(params_dict) + # worker.computeRGD(InitX_RGD, Ch_svd, Md_tr, Md_alp, Md_sig) + worker.computeRGD(InitX_RGD, Ch_svd) + + Plt_Err_Time(worker) diff --git a/src/qibo/tomography_RGD/Run_example.ipynb b/src/qibo/tomography_RGD/Run_example.ipynb new file mode 100644 index 0000000000..9cc5dd48ed --- /dev/null +++ b/src/qibo/tomography_RGD/Run_example.ipynb @@ -0,0 +1,600 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The code is to run the RGD tomography method \n", + "1. prepare the quantum state\n", + "2. do the measurement\n", + "3. do the RGD optimization method to reconstruct the state" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "#import qutip as qu\n", + "#from states import GHZState, HadamardState, RandomState\n", + "from qibo_states import GHZState, HadamardState, RandomState\n", + "from qibo import quantum_info\n", + "\n", + "import methodsRGD_core\n", + "import methodsMiFGD_core\n", + "\n", + "import measurements\n", + "import projectors\n", + "\n", + "from BasicTools import Plt_Err_Time\n", + "from BasicTools import Generate_All_labels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## variables\n", + "- n = number of qubits\n", + "- labels = list of sampled Pauli operators \n", + "- Nr = rank of the target density matrix " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "#n = 3; labels = Generate_All_labels(n)\n", + "#n = 3; labels = projectors.generate_random_label_list(50, n)\n", + "n = 4; labels = projectors.generate_random_label_list(120, n)\n", + "\n", + "\n", + "num_labels = len(labels)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[Qibo 0.2.12|INFO|2024-10-11 11:21:38]: Using numpy backend on /CPU:0\n", + "[Qibo 0.2.12|INFO|2024-10-11 11:21:38]: Using numpy backend on /CPU:0\n" + ] + } + ], + "source": [ + "Nr = 1 # rank of the target density matrix\n", + "\n", + "#state = GHZState(n)\n", + "#state = HadamardState(n)\n", + "state = RandomState(n)\n", + "\n", + "target_density_matrix = state.get_state_matrix()\n", + "target_state = state.get_state_vector() " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Do the shot measurement" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[Qibo 0.2.12|INFO|2024-10-11 11:21:38]: Using numpy backend on /CPU:0\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ------------------- start calc measurment_list ------------------ \n", + "\n", + " **** directly calc & save measurement_list \n", + " **** len(label_list) = 120, len(data_dict) = 120\n", + "\n", + " ------------- DONE of calculating measurment_list ------------------ \n", + "\n" + ] + } + ], + "source": [ + "state.create_circuit()\n", + "data_dict_list = state.execute_measurement_circuits(labels)\n", + "#print(data_dict_list)\n", + "\n", + "data_dict = {}\n", + "for ii in range(num_labels):\n", + " label = data_dict_list[ii]['label']\n", + " data_dict[label] = data_dict_list[ii]['count_dict'] \n", + " \n", + "measurement_list = measurements.MeasurementStore.calc_measurement_list(labels, data_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### construct Pauli matrix Projectors\n", + "1. construct the Pauli matrix\n", + "2. calculate the exact coefficient" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Partion_Pj = 0 --> No partion in Pj\n", + "\n", + " *** Pj_file = ./testData/qibo/Pj_list.pickle is saved (i.e. dump)\n", + " *** label_file = ./testData/qibo/labels.pickle is saved\n", + "\n", + " pool.map COMPLETED by #CPU = 16 \n", + "\n", + " [projectors] num_cpus = 16, saved bulk Pj = 1 \n", + "\n" + ] + } + ], + "source": [ + "projector_store_path = './testData/qibo'\n", + "projector_store = projectors.ProjectorStore(labels)\n", + "\n", + "# 1. construct the sampled Pauli operators\n", + "num_cpus, saveP_bulk, Partition_Pj = projector_store.mpPool_map(projector_store_path)\n", + "projector_dict = projectors.ProjectorStore.load_PoolMap(projector_store_path, labels)\n", + "\n", + "# 2. calculate the exact coefficients for sampled Pauli operators\n", + "projector_list = [projector_dict[label] for label in labels]\n", + "yProj_Exact = methodsRGD_core.Amea(projector_list, target_density_matrix, num_labels, 1) # argv[-1] = coef\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "params_dict = { 'Nr': Nr,\n", + " 'target_DM': target_density_matrix,\n", + " 'labels': labels,\n", + " 'measurement_list': measurement_list,\n", + " 'projector_list': projector_list,\n", + " 'num_iterations': 150,\n", + " 'convergence_check_period': 1 \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### DO the RGD tomography optimization method" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " self.coef = 0.3651483716701107\n", + " InitX = 1\n", + " ********* using randomized-SVD to construct X0 = uk @ sDiag @ vkh **************************\n", + " *** 0-th (A A*) done: Time --> ATQ: 0.00041604042053222656, AG: 0.00039386749267578125 ***\n", + " *** 1-th (A A*) done: Time --> ATQ: 0.00038814544677734375, AG: 0.00039386749267578125 ***\n", + " *** 2-th (A A*) done: Time --> ATQ: 0.00038909912109375, AG: 0.00039076805114746094 ***\n", + " *** 3-th (A A*) done: Time --> ATQ: 0.0003917217254638672, AG: 0.00039124488830566406 ***\n", + " *** 4-th (A A*) done: Time --> ATQ: 0.00038504600524902344, AG: 0.0004038810729980469 ***\n", + " *** 5-th (A A*) done: Time --> ATQ: 0.00042319297790527344, AG: 0.00038886070251464844 ***\n", + " *** 6-th (A A*) done: Time --> ATQ: 0.0004608631134033203, AG: 0.0004260540008544922 ***\n", + " *** 7-th (A A*) done: Time --> ATQ: 0.0003910064697265625, AG: 0.00041103363037109375 ***\n", + " *** 8-th (A A*) done: Time --> ATQ: 0.0004200935363769531, AG: 0.0003972053527832031 ***\n", + " *** 9-th (A A*) done: Time --> ATQ: 0.0003871917724609375, AG: 0.0003838539123535156 ***\n", + " *** 10-th (A A*) done: Time --> ATQ: 0.0004191398620605469, AG: 0.0003859996795654297 ***\n", + " *** 11-th (A A*) done: Time --> ATQ: 0.0003859996795654297, AG: 0.0003879070281982422 ***\n", + " *** 12-th (A A*) done: Time --> ATQ: 0.00039076805114746094, AG: 0.00041294097900390625 ***\n", + " *** 13-th (A A*) done: Time --> ATQ: 0.0003838539123535156, AG: 0.00038695335388183594 ***\n", + " *** 14-th (A A*) done: Time --> ATQ: 0.00038886070251464844, AG: 0.0003871917724609375 ***\n", + " self.rSVD --> time = 0.02103900909423828\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " -------- X0 --> (-1)-th iteration [ InitX = 1, SVD choice = -1 ] \n", + "\n", + " X0 step (RGD) --> time = 0.021294116973876953\n", + "\n", + " X0 --> Tr(X0) = (1.075619697065995+1.2902005852577503e-17j)\n", + "\n", + " X0 --> Target Err = 0.3621551963698314\n", + "\n", + " RGD max num_iterations = 150\n", + "\n", + " -------- 0-th iteration \n", + "\n", + " 0-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0010519027709960938\n", + " [calc_n1_n2] AptGk --> time = 0.0005068778991699219\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 1.0554144248593634e-17\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [0.91692412 0.01786955]\n", + " max(s2r) = 0.9169241181110029\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " **** 0-th iteratation\n", + "\n", + " min(uk +/- uk_old) = 1.9951921519475475\n", + "\n", + " relative_errU = 1.9951921519475475, relative_errX = 0.2331502591214293\n", + " Target_Error = 0.27158612141597993\n", + "\n", + " convergence check --> time = 0.009998083114624023\n", + "\n", + " stepRGD --> time = 0.013380765914916992\n", + "\n", + " -------- 1-th iteration \n", + "\n", + " 1-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0012710094451904297\n", + " [calc_n1_n2] AptGk --> time = 0.0002300739288330078\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 2.5651930935870073e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [0.96052057 0.00597897]\n", + " max(s2r) = 0.9605205664421295\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.07871344847499537\n", + "\n", + " relative_errU = 0.07871344847499537, relative_errX = 0.12337481502208704\n", + " Target_Error = 0.25155147052028326\n", + "\n", + " convergence check --> time = 0.00020813941955566406\n", + "\n", + " stepRGD --> time = 0.0028412342071533203\n", + "\n", + " -------- 2-th iteration \n", + "\n", + " 2-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0003819465637207031\n", + " [calc_n1_n2] AptGk --> time = 0.00020194053649902344\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 7.09679987382298e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [0.96223651 0.00124243]\n", + " max(s2r) = 0.9622365124613699\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.03591577238724454\n", + "\n", + " relative_errU = 0.035915772387244534, relative_errX = 0.050861108363104084\n", + " Target_Error = 0.24626481804473999\n", + "\n", + " convergence check --> time = 0.00015497207641601562\n", + "\n", + " stepRGD --> time = 0.0011680126190185547\n", + "\n", + " -------- 3-th iteration \n", + "\n", + " 3-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.000370025634765625\n", + " [calc_n1_n2] AptGk --> time = 0.00019788742065429688\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 2.9378698038347618e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.73604527e-01 5.92807851e-04]\n", + " max(s2r) = 0.973604527283547\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0246698521325734\n", + "\n", + " relative_errU = 0.024669852132573392, relative_errX = 0.037026617441051866\n", + " Target_Error = 0.25111289357056593\n", + "\n", + " convergence check --> time = 0.0001671314239501953\n", + "\n", + " stepRGD --> time = 0.001135110855102539\n", + "\n", + " -------- 4-th iteration \n", + "\n", + " 4-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0003693103790283203\n", + " [calc_n1_n2] AptGk --> time = 0.00020599365234375\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 2.9406271179488805e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.75734376e-01 2.27291229e-04]\n", + " max(s2r) = 0.9757343763860515\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.015261161994985345\n", + "\n", + " relative_errU = 0.015261161994985345, relative_errX = 0.021715973390462582\n", + " Target_Error = 0.2507106704191409\n", + "\n", + " convergence check --> time = 0.00014209747314453125\n", + "\n", + " stepRGD --> time = 0.0010960102081298828\n", + "\n", + " -------- 5-th iteration \n", + "\n", + " 5-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0003819465637207031\n", + " [calc_n1_n2] AptGk --> time = 0.00021314620971679688\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 4.423096493486294e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.80586401e-01 1.03562616e-04]\n", + " max(s2r) = 0.9805864013821034\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.01027640883355472\n", + "\n", + " relative_errU = 0.01027640883355472, relative_errX = 0.015394202837662211\n", + " Target_Error = 0.2545467809572611\n", + "\n", + " convergence check --> time = 0.00013780593872070312\n", + "\n", + " stepRGD --> time = 0.0011048316955566406\n", + "\n", + " -------- 6-th iteration \n", + "\n", + " 6-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.00036907196044921875\n", + " [calc_n1_n2] AptGk --> time = 0.00019693374633789062\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 4.779599312468092e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.81884055e-01 4.44985552e-05]\n", + " max(s2r) = 0.9818840552946526\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.006731866125160581\n", + "\n", + " relative_errU = 0.006731866125160581, relative_errX = 0.00961801429725703\n", + " Target_Error = 0.25480395806022166\n", + "\n", + " convergence check --> time = 0.00013399124145507812\n", + "\n", + " stepRGD --> time = 0.001068115234375\n", + "\n", + " -------- 7-th iteration \n", + "\n", + " 7-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.000370025634765625\n", + " [calc_n1_n2] AptGk --> time = 0.00019788742065429688\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 6.235474184076634e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.83881931e-01 1.98100656e-05]\n", + " max(s2r) = 0.9838819311458938\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0044871251814520155\n", + "\n", + " relative_errU = 0.004487125181452015, relative_errX = 0.006670117874712933\n", + " Target_Error = 0.2566533526099351\n", + "\n", + " convergence check --> time = 0.0001380443572998047\n", + "\n", + " stepRGD --> time = 0.0011088848114013672\n", + "\n", + " -------- 8-th iteration \n", + "\n", + " 8-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0018188953399658203\n", + " [calc_n1_n2] AptGk --> time = 0.0005021095275878906\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 1.4467622285056774e-17\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.84554830e-01 9.04372353e-06]\n", + " max(s2r) = 0.9845548295849362\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0030307644836314587\n", + "\n", + " relative_errU = 0.003030764483631458, relative_errX = 0.004341812765443025\n", + " Target_Error = 0.2568509363937573\n", + "\n", + " convergence check --> time = 0.00042700767517089844\n", + "\n", + " stepRGD --> time = 0.003592967987060547\n", + "\n", + " -------- 9-th iteration \n", + "\n", + " 9-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0010230541229248047\n", + " [calc_n1_n2] AptGk --> time = 0.0007970333099365234\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 2.455857350530492e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.85393430e-01 3.96628441e-06]\n", + " max(s2r) = 0.9853934295026873\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0020062564215114318\n", + "\n", + " relative_errU = 0.0020062564215114313, relative_errX = 0.002963522498080914\n", + " Target_Error = 0.25767285656921923\n", + "\n", + " convergence check --> time = 0.0003657341003417969\n", + "\n", + " stepRGD --> time = 0.0034308433532714844\n", + "\n", + " -------- 10-th iteration \n", + "\n", + " 10-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.0008909702301025391\n", + " [calc_n1_n2] AptGk --> time = 0.00028014183044433594\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 2.4270114346342185e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.85715704e-01 1.88286122e-06]\n", + " max(s2r) = 0.9857157035892445\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0013820794413388258\n", + "\n", + " relative_errU = 0.0013820794413388255, relative_errX = 0.0019820437041024406\n", + " Target_Error = 0.2577708636994716\n", + "\n", + " convergence check --> time = 0.00019097328186035156\n", + "\n", + " stepRGD --> time = 0.002341747283935547\n", + "\n", + " -------- 11-th iteration \n", + "\n", + " 11-th step -> self.Remain_Hermitian = 1, self.EigV_positive = 1\n", + " calc_PtG_2_uG_Gv --> time = 0.00039196014404296875\n", + " [calc_n1_n2] AptGk --> time = 0.00020194053649902344\n", + "\n", + " ************ 0-th scaling for the Alpha\n", + " Hr_tangentWk --> Remain_Hermitian = 1, EigV_positve = 1\n", + " D2s - D2s.T.conj() = 5.785507140965031e-18\n", + " np.allclose(D2s, D2s.T.conj()) = True\n", + "\n", + " s2r = [9.86074695e-01 8.19476417e-07]\n", + " max(s2r) = 0.9860746946335668\n", + " ------------ DONE svd of D2s with 0-th scaling (#max sc=1)\n", + " Hermitian ? Remain_Hermiain = 1, EigV_postive = 1\n", + " error of Hermitian = LA.norm(|uk| - |vk|) = 0.0\n", + " min(uk +/- uk_old) = 0.0009116186211432837\n", + "\n", + " ********* XkErrRatio = 0.0013399025158102797 < StpTol ******\n", + " relative_errU = 0.0009116186211432835, relative_errX = 0.0013399025158102797\n", + " Target_Error = 0.25813056518851835\n", + "\n", + " convergence check --> time = 0.00016117095947265625\n", + "\n", + " stepRGD --> time = 0.0011601448059082031\n", + "\n" + ] + } + ], + "source": [ + "Ch_svd = -1 # choice for initial SVD (0: LA.svd, 1: svds; 2: power_Largest_EigV, -1: rSVD)\n", + "InitX_RGD = 1; # method of choosing initial X0\n", + "\n", + "worker = methodsRGD_core.BasicWorkerRGD(params_dict)\n", + "worker.computeRGD(InitX_RGD, Ch_svd)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot the result" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Plt_Err_Time(worker)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qiskit10", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/qibo/tomography_RGD/measurements.py b/src/qibo/tomography_RGD/measurements.py new file mode 100755 index 0000000000..6ea6129144 --- /dev/null +++ b/src/qibo/tomography_RGD/measurements.py @@ -0,0 +1,843 @@ +import multiprocessing +import os +import pickle +import time + +import numpy as np + +# ----------------------------------------- # +# class method to deal with each label # +# ----------------------------------------- # + + +class Measurement: + """to deal with only label measurement results""" + + def __init__(self, label, count_dict): + """initialization of shot measurement results for a given label + according to the given count dict + + Args: + label (str): a Pauli string corresponding to the sampled Pauli operator + (e.g. XYZXX) + count_dict (dict): counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + + Initializes Measurement class + - label: a string representing a Pauli matrix (e.g. XYZXX) + - count_dict: counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + - num_shots: number of shots measurement is taken to get empirical frequency through counts + """ + + akey = list(count_dict.keys())[0] + assert len(label) == len(akey) + + self.label = label + self.count_dict = self.zfill(count_dict) + self.num_shots = sum(count_dict.values()) + + @staticmethod + def zfill(count_dict): + """to pad 0 in the front of the keys of the count_dict such that + their lengths are equal to the qubit number. + + Args: + count_dict (dict): counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + + Returns: + dict: (result) same as count_dict but with the keys being filled with 0 in + the front to let their lengths be equal to the qubit number. + """ + n = len(list(count_dict.keys())[0]) + d = 2**n + result = {} + for idx in range(d): + key = bin(idx)[2:] + key = key.zfill(n) + result[key] = count_dict.get(key, 0) + return result + + @staticmethod + def naive_parity(key): + """Calculates the number of '1' in the given key + + Args: + key (str): the measurement outcome in the form of 0 or 1 + i.e. possible output binary string + (eg) '101' or '110' for the 3 qubit measurement + + Returns: + int: the number of '1' in the key + """ + return key.count("1") + + def effective_parity(self, key): + """Calculates the effective number of '1' in the given key + + Args: + key (str): the measurement outcome in the form of 0 or 1 + (eg) '101' or '110' for the 3 qubit measurement + Returns: + int: the number of effective '1' in the key + """ + indices = [i for i, symbol in enumerate(self.label) if symbol == "I"] + digit_list = list(key) + for i in indices: + digit_list[i] = "0" + effective_key = "".join(digit_list) + + return effective_key.count("1") + + def parity(self, key, parity_flavor="effective"): + """determine the (effective) number of '1' in the key + according to parity_flavor + Args: + key (str): the measurement outcome in the form of 0 or 1 + (eg) '101' or '110' for the 3 qubit measurement + parity_flavor (str, optional): Determine different ways of calculating parity. + Defaults to 'effective'. + + Returns: + int: the (effective) number of '1' in the key + according to parity_flavor + """ + if parity_flavor == "effective": + return self.effective_parity(key) + else: + return self.naive_parity(key) + + def get_pauli_correlation_measurement(self, beta=None, parity_flavor="effective"): + """Generate Pauli correlation measurement (expectation value of Pauli monomials). + Note that summation of d=2^n Pauli basis measurement corresponds to one Pauli correlation measurement. + + Args: + beta (float, optional): for shifting the counted number (not used now). Defaults to None. + parity_flavor (str, optional): Determine different ways of calculating parity. Defaults to 'effective'. + + Returns: + dict: dictionary with Pauli label as the key and its corresponding coefficient in the density matrix + expansion as the value of the dictionary + """ + + if beta == None: + # beta = 0.50922 # original MiFGD usage + beta = 0.0 # this one looks more exact + num_shots = 1.0 * self.num_shots + num_items = len(self.count_dict) + + # frequencies = {k : (v + beta) / (num_shots + num_items * beta) for k, v in self.count_dict.items()} + # parity_frequencies = {k : (-1) ** self.parity(k, parity_flavor) * v for k, v in frequencies.items()} + # correlation = sum(parity_frequencies.values()) + # data = {self.label : correlation} + + freq2 = {k: (v) / (num_shots) for k, v in self.count_dict.items()} + parity_freq2 = { + k: (-1) ** self.parity(k, parity_flavor) * v for k, v in freq2.items() + } + correlation2 = sum(parity_freq2.values()) + data2 = {self.label: correlation2} + + return data2 + + def get_pauli_basis_measurement(self, beta=None): + """Generate Pauli basis measurement. + Note that summation of d=2^n Pauli basis measurement corresponds to one Pauli correlation measurement. + + Args: + beta (float, optional): for shifting the counted number. Defaults to None. + + Returns: + dict: dictionary with Pauli label as the key and + the effective frequencies for each possible binary outcomes as the value of the dictionary + """ + + if beta == None: + beta = 0.50922 + num_shots = 1.0 * self.num_shots + num_items = len(self.count_dict) + frequencies = { + k: (v + beta) / (num_shots + num_items * beta) + for k, v in self.count_dict.items() + } + data = {self.label: frequencies} + return data + + def _pickle_save(self, fpath): + """to save the count_dict in the pickle format, where + count_dict is a dictionary + that counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + + Args: + fpath (str): the path to store count_dict + """ + with open(fpath, "wb") as f: + pickle.dump(self.count_dict, f) + + def _hdf5_save(self, fpath): + """to save the count_dict in the hdf5 format + + Args: + fpath (str): the path to store count_dict + """ + f = h5py.File(fpath, "a") + group = f.create_group(self.label) + + items = [[key, value] for key, value in self.count_dict.items()] + keys = np.array([item[0] for item in items], dtype="S") + values = np.array([item[1] for item in items], dtype="int32") + + dataset = group.create_dataset("keys", data=keys) + dataset = group.create_dataset("values", data=values) + f.close() + + # Save the measurement to disk + def save(self, path): + """to save the count_dict either in the pickle or hdf5 format + + Args: + fpath (str): the path to store count_dict + """ + if os.path.isdir(path): + fpath = os.path.join(path, "%s.pickle" % self.label) + self._pickle_save(fpath) + elif path.endswith(".hdf5"): + fpath = path + self._hdf5_save(fpath) + + @classmethod + def _pickle_load(cls, fpath): + """to load count_dict from the saved pickle file + + Args: + fpath (str): the path of the stored count_dict + Returns: + dict: (count_dict) which is a dictionary + that counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + """ + with open(fpath, "rb") as f: + data = pickle.load(f) + return data + + @classmethod + def _hdf5_load(cls, fpath, label): + """to load count_dict from the saved hdf5 file + + Args: + fpath (str): the path of the stored count_dict + label (str): the Pauli operator label + + Returns: + dict: (count_dict) which is a dictionary + that counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + """ + + f = h5py.File(fpath, "r") + group = f[label] + keys = group["keys"][:] + values = group["values"][:] + + data = {k: v for k, v in zip(*[keys, values])} + return data + + # Load a measurement from disk + @classmethod + def load(cls, path, label, num_leading_symbols=0): + """to load a count_dict from either pickle file or h5py file + and convert it to the class Measurement object + + Args: + path (str): the path of the stored count_dict + label (str): the Pauli operator label + num_leading_symbols (int, optional): number of effective qubit numbers in the labels. Defaults to 0. + + Returns: + cls: the object of class Measurement + """ + if os.path.isdir(path): + if num_leading_symbols == 0: + fpath = os.path.join(path, "%s.pickle" % label) + count_dict = cls._pickle_load(fpath) + else: + fragment_name = label[:num_leading_symbols] + fpath = os.path.join(path, fragment_name, "%s.pickle" % label) + count_dict = cls._pickle_load(fpath) + elif path.endswith(".hdf5"): + fpath = path + count_dict = cls._hdf5_load(fpath, label) + measurement = cls(label, count_dict) + return measurement + + +# --------------------------------------------- # +# functions to calculate measurement_list # +# --> good for parallel calc # +# --------------------------------------------- # +def measurement_list_calc(label_list, count_dict_list): + """to calculate measurement_list corresponding to coefficients of the target density matrix + basis expansion respect to label_list representing sampled Pauli operators + + Args: + label_list (list): list of label names representing the sampled Pauli operators + i.e. each label is a Pauli string, e.g. XYZXX + count_dict_list (list): list of dictionaries where each dictionary is a count_dict + - count_dict: counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + Returns: + list: (measurement_list) list of coefficients corresponding to basis expansions + in each sampled Pauli operator + """ + + parity_flavor = "effective" + beta = None + measurement_object_list = [ + Measurement(label, count_dict) + for (label, count_dict) in zip(*[label_list, count_dict_list]) + ] + + measurement_list = [ + measurement_object.get_pauli_correlation_measurement(beta, parity_flavor)[label] + for (label, measurement_object) in zip(*[label_list, measurement_object_list]) + ] + # print('label_list = {}'.format(label_list)) + # print('measurement_list = {}'.format(measurement_list)) + return measurement_list + + +def measurement_list_calc_wID_wrap(argv): + """a wrapper function to call measurement_list_calc_wID + + Args: + argv (tuple): all the necessary arguments for calling measurement_list_calc_wID + + Returns: + dir: {ID: measurement_list} which is the output of measurement_list_calc_wID + """ + + out = measurement_list_calc_wID(*argv) + return out + + +def measurement_list_calc_wID(ID, label_list, count_dict_list): + """same as measurement_list_calc but with given ID as a index for specifying different cpu in parallel computation. + The effect is to calculate measurement_list corresponding to coefficients of the target density matrix + basis expansion respect to label_list representing sampled Pauli operators + + Args: + ID (int): an index for parallel computation usage + label_list (list): list of label names representing the sampled Pauli operators + i.e. each label is a Pauli string, e.g. XYZXX + count_dict_list (list): list of dictionaries where each dictionary is a count_dict + - count_dict: counts of each possible binary output string (e.g. '000...00', '000...01', etc.) + For example, count_dict = {'101': 617, '000': 581, '111': 574, '010': 628} + + Returns: + dir: {ID: measurement_list}, where + ID is the parallel computation index, and + measurement_list is list of coefficients corresponding to basis expansions + in each sampled Pauli operator + """ + + print(" start to calc {}-th label_list".format(ID)) + + parity_flavor = "effective" + beta = None + measurement_object_list = [ + Measurement(label, count_dict) + for (label, count_dict) in zip(*[label_list, count_dict_list]) + ] + + measurement_list = [ + measurement_object.get_pauli_correlation_measurement(beta, parity_flavor)[label] + for (label, measurement_object) in zip(*[label_list, measurement_object_list]) + ] + + return {ID: measurement_list} + + +# ----------------------------- # +# for parallel CPU # +# ----------------------------- # +def split_list(x, num_parts): + """to slice list x into num_parts separated elements + + Args: + x (list): list of elements which are supposed to be Pauli operator labels + num_parts (int): the number of parts we want to split x + + Returns: + list: list of separated list x into num_parts elments. + (eg) x = ['YYY', 'XXZ', 'XYY', 'YIY', 'ZIZ', 'XXY', 'IXZ', 'IIX', 'ZXY', 'IIZ'] + then split_list(x, 3) = [['YYY', 'XXZ', 'XYY'], ['YIY', 'ZIZ', 'XXY'], ['IXZ', 'IIX', 'ZXY', 'IIZ']] + """ + n = len(x) + size = n // num_parts + parts = [x[i * size : (i + 1) * size] for i in range(num_parts - 1)] + parts.append(x[(num_parts - 1) * size :]) + return parts + + +# --------------------------------- # +# class to deal with all labels # +# --------------------------------- # + + +class MeasurementStore: + """to deal with measurement results for a list of labels""" + + def __init__(self, measurement_dict): + """to initialize the measurement results for a list of sampled Pauli labels + + Args: + measurement_dict (dict): a dictionary of shot measurement results + + (eg) measurement_dict = + {'YXI': {'101': 300, '110': 306, '111': 282, '000': 307, '001': 319, '010': 283, '100': 291, '011': 312}, + 'ZZI': {'000': 1181, '111': 1219}, + 'YXX': {'101': 294, '010': 291, '110': 291, '100': 293, '001': 322, '111': 295, '011': 286, '000': 328}, + 'ZIZ': {'000': 1193, '111': 1207}, + 'ZYY': {'100': 313, '010': 299, '101': 289, '011': 277, '110': 308, '001': 319, '000': 302, '111': 293}, + 'XXZ': {'101': 292, '001': 321, '100': 288, '010': 284, '000': 298, '110': 299, '011': 328, '111': 290}, + 'ZYI': {'101': 617, '000': 581, '111': 574, '010': 628}, + 'YXZ': {'001': 295, '101': 317, '111': 328, '110': 300, '010': 306, '000': 288, '100': 279, '011': 287}, + 'YYI': {'001': 290, '101': 291, '010': 307, '111': 299, '100': 284, '000': 329, '110': 299, '011': 301}, + 'IIZ': {'000': 1220, '111': 1180}} + + Initializes MeasurementStore class + - measurement_dict: the shot measurement result which is given when calling the class constructor + - labels: list of strings of Pauli matrices (where each label is a Pauli matrix, e.g. XYZXX) + - size : number of labels + """ + self.measurement_dict = measurement_dict + + self.labels = list(measurement_dict.keys()) + self.size = len(self.labels) + + @classmethod + def calc_measurement_list(cls, label_list, data_dict, method=0): + """calculate the measurement list which is the list of coefficients corresponding to each label (Pauli projector) + + Args: + label_list (list): list of labels of Pauli operators + data_dict (dict): dictionary storing all shot measurement results of each label + method (int, optional): method of converting data_dict to measurement_list. Defaults to 0. + where the measurement_list is a list of numbers corresponding to coefficients of Pauli operators + in the basis expansion of the density matrix of interest + + Returns: + list: (measurement_list) list of float numbers corresponding to coefficients of Pauli operators + """ + + print( + " ------------------- start calc measurment_list ------------------ \n" + ) + + count_dict_list = [data_dict[label] for label in label_list] + del data_dict + + if method == 0: + print(" **** directly calc & save measurement_list ") + print( + " **** len(label_list) = {}, len(data_dict) = {}\n".format( + len(label_list), len(count_dict_list) + ) + ) + measurement_list = measurement_list_calc(label_list, count_dict_list) + + elif method == 1: # parallel CPU + + num_CPU = multiprocessing.cpu_count() + if num_CPU > len(label_list): + num_CPU = 3 + print( + " **** use parallel #CPU = {} to calc measurement_list \n".format( + num_CPU + ) + ) + + ID_list = np.arange(num_CPU) + label_part = split_list(label_list, num_CPU) + count_dict_part = split_list(count_dict_list, num_CPU) + + del count_dict_list + + pool = multiprocessing.Pool(num_CPU) + + Run_parallel = 2 + + if Run_parallel == 1: + L_pt = pool.starmap( + measurement_list_calc_wID, zip(ID_list, label_part, count_dict_part) + ) + pool.close() + pool.join() + + ml_dict = {} + { + ml_dict.update(xx) for xx in L_pt + } # if return {ID, measurement_list} + + elif Run_parallel == 2: + + ml_dict = {} + mp_collect = [] + for ii, labels in enumerate(label_part): + print( + " ************** {}-th label_part to be parallelly calc *******".format( + ii + ) + ) + + mp_out = pool.apply_async( + measurement_list_calc_wID_wrap, + ([ii, labels, count_dict_part[ii]],), + ) + mp_collect.append(mp_out) + pool.close() + pool.join() + del count_dict_part + + for xx in mp_collect: + out = xx.get() + + ml_dict.update(out) + + measurement_list = [] + for xx in ID_list: + measurement_list += ml_dict[xx] + + print( + " ------------- DONE of calculating measurment_list ------------------ \n" + ) + + return measurement_list + + @classmethod + def Save_measurement_by_data_dict( + cls, path, label_list, data_dict, method=0, Name=None, ToSave=1 + ): + """to obtain the measurement_list (numbers corresponding to coefficients of each Pauli operator) + from the given data_dict (the direct shot measurements for each Pauli operator) + + Args: + path (str): directory path of saving the measurement results + label_list (list): list of Pauli operator labels + data_dict (dict): dictionary of shot measurement results + method (int, optional): method of coverting data_dict to measurement_list. Defaults to 0. + Name (str, optional): the name appendix of the file saving the results. Defaults to None. + ToSave (int, optional): To save the measurement_list into the file or not. Defaults to 1. + + Returns: + list: (label_list) list of Pauli operator labels + list: (measurement_list) list of numbers representing the coefficients corresponding to + each Pauli operator + """ + + tt1 = time.time() + measurement_list = cls.calc_measurement_list(label_list, data_dict, method) + tt2 = time.time() + print( + " ****** cls.calc_measurement_list --> time = {} *****\n".format( + tt2 - tt1 + ) + ) + + if ToSave == 1: + tt3 = time.time() + cls.Save_measurement_list_file(path, label_list, measurement_list, Name) + tt4 = time.time() + print( + " ****** cls.Save_measurement_list_file --> time = {} ****".format( + tt4 - tt3 + ) + ) + + return label_list, measurement_list + + def Save_measurement_list(self, path, method=0, Name=None): + """to call self.calc_measurement_list function to convert + self.data_dict (shot measurement record) into measurement_list (coefficients of labels) + & save the results by calling self.Save_measurement_list_file + + Args: + path (str): directory path storing the measurements results + method (int, optional): method passing to self.calc_measurement_list function. Defaults to 0. + Name (str, optional): name appendix of files for storing the measurement results. Defaults to None. + + Returns: + list: (label_list) list of Pauli operator labels + list: (measurement_list) list of Pauli operator coefficients in the expansion of the density matrix + calculated from self.data_dict (shot measurement data for each label). + """ + print(" ------------- to calc & save measurement_list -------------- \n") + + label_list = self.label_list + data_dict = self.data_dict + + measurement_list = self.calc_measurement_list(label_list, data_dict, method) + + self.Save_measurement_list_file(path, label_list, measurement_list, Name) + + return label_list, measurement_list + + @classmethod + def Load_measurement_list(cls, path, Name=None): + """to load the measurement list of Pauli operator coefficients from the specified path + + Args: + path (str): the path to the stored measurement list + Name (str, optional): suffix of the file name. Defaults to None. + + Returns: + list : (label_list) list of Pauli operator labels + list : (measurement_list) list of coefficients of the density matrix + in the basis expansion corresponding to the labels + """ + + if Name == None: + ml_file = "{}/measurement_list.pickle".format(path) + print(" ml_file = {}".format(ml_file)) + + with open(ml_file, "rb") as f: + label_list, measurement_list = pickle.load(f) + + else: + ml_file = "{}/measureL_{}.pickle".format(path, Name) + + with open(ml_file, "rb") as f: + Name, label_list, measurement_list = pickle.load(f) + + print( + " --> loading measurement_list DONE from ml_file = {}\n".format(ml_file) + ) + return label_list, measurement_list + + @classmethod + def Save_measurement_list_file(cls, path, label_list, measurement_list, Name=None): + """to save the calculated measurement_list into a file + + Args: + path (str): directory path of saved measurement_list + label_list (list): list of labels + measurement_list (list): list of coefficients for each Pauli operator label + calculated from shot measurements + Name (str, optional): name appendix of files storing the results. Defaults to None. + """ + + print("\n") + print(" path = {} \n".format(path)) + print(" Name = {} \n".format(Name)) + if Name == None: + ml_file = "{}/measurement_list.pickle".format(path) + + with open(ml_file, "wb") as f: + pickle.dump([label_list, measurement_list], f) + + else: + ml_file = "{}/measureL_{}.pickle".format(path, Name) + + with open(ml_file, "wb") as f: + pickle.dump([Name, label_list, measurement_list], f) + + print( + " *** measurement_list --> ml_file = {} is saved\n".format( + ml_file + ) + ) + + def saveInOne(self, path, Name=None): + """to save all the Pauli operator labels and the corresponding data_dict into files, + instead of saving each individual Pauli operator label and data_dict into a single file + + Args: + path (str): the path to save the measurement results + Name (str, optional): the suffix to the file name to save. Defaults to None. + """ + format = "pickle" + if not os.path.exists(path): + os.mkdir(path) + + label_list = sorted(self.labels) + + data_dict = {} + for label in label_list: + data_dict[label] = self.measurement_dict[label] + + self.label_list = label_list + self.data_dict = data_dict + + # -------- save files ---------------- # + if Name == None: + # -------- saved file for labels ---------------- # + label_file = "{}/labels.pickle".format(path) + + with open(label_file, "wb") as f: + pickle.dump(label_list, f) + print(" *** label list is stored in {} \n".format(label_file)) + + # -------- saved file for count_dict ------------- # + Fname = "{}/count_dict.pickle".format(path) + + with open(Fname, "wb") as f: + pickle.dump(data_dict, f) + print( + " *** count_dict file = {} is dumped (i.e. saved) \n".format( + Fname + ) + ) + + else: + label_count_file = "{}/labs_data_{}.pickle".format(path, Name) + + with open(label_count_file, "wb") as f: + pickle.dump([label_list, data_dict], f) + print( + " *** label_list & count_dict_list is stored in {} \n".format( + label_count_file + ) + ) + + @classmethod + def load_labels_data(cls, path, Name): + """to load Pauli operator labels and shot measurement data from path + + Args: + path (str): the path to save the measurement results + Name (str, optional): the suffix to the file name to save. Defaults to None. + + Returns: + list : (label_list) list of Pauli operator labels + list : data_dict (dict): dictionary storing all shot measurement results of each label + + """ + label_count_file = "{}/labs_data_{}.pickle".format(path, Name) + print(" label_count_file = {}\n".format(label_count_file)) + + with open(label_count_file, "rb") as f: + label_list, data_dict = pickle.load(f) + + return label_list, data_dict + + # Load measurements previously saved under a disk folder + @classmethod + def load_OneDict(cls, path, labels=None): + """to load labels and shot measurement results from stored files + + Args: + path (str): the path to save the measurement results + labels (list, optional): list of Pauli operator labels. Defaults to None. + + Returns: + dict: (measurement_dict) shot measurement results + """ + + if labels == None: + labels = cls.load_labels_from_file(path) + + Fname = "{}/count_dict.pickle".format(path) + print(" ---> Loading count_dict from {}\n".format(Fname)) + with open(Fname, "rb") as f: + measurement_dict = pickle.load(f) + print(" ---> count_dict is loaded \n") + + return measurement_dict + + @classmethod + def load_labels_from_file(cls, path): + """Load a list of labels from labels.pickle in the path + + Args: + path (str): the path to save the measurement results + + Returns: + list: list of sampled Pauli operator labels + """ + + label_file = "{}/labels.pickle".format(path) + with open(label_file, "rb") as f: + labels = pickle.load(f) + print(" ---> label_file = {} is loaded \n".format(label_file)) + + return labels + + def save(self, path): + """to save each Pauli operator label and its corresponding shot measurement data in + each individual file. + + Args: + path (str): the path to save the measurement results + """ + format = "hdf5" + if not path.endswith(".hdf5"): + format = "pickle" + if not os.path.exists(path): + os.mkdir(path) + for label, count_dict in self.measurement_dict.items(): + Measurement(label, count_dict).save(path) + + @classmethod + def load_labels(cls, path): + """to load all the Pauli labels from collecting all the stored files in the path + representing each individual label + + Args: + path (str): the path to load the saved measurement results + + Returns: + list: list of sampled Pauli operator labels + """ + if path.endswith(".hdf5"): + with h5py.File(path, "r") as f: + labels = f.keys() + else: + try: + labels = [fname.split(".")[0] for fname in os.listdir(path)] + except: + fragment_paths = [ + os.path.join(path, fragment) for fragment in os.listdir(path) + ] + labels = [] + for fragment_path in fragment_paths: + fragment_labels = [ + fname.split(".")[0] for fname in os.listdir(fragment_path) + ] + labels.extend(fragment_labels) + return labels + + # Load measurements previously saved under a disk folder + @classmethod + def load(cls, path, labels=None): + """to load all the shot measurement results from all the stored files, where each file + contains only one label information + + Args: + path (str): the path to load the saved measurement results + labels (list, optional): list of Pauli operator labels. Defaults to None. + + Returns: + dict: (measurement_dict) collecting all the shot measurements results for all the labels + """ + + if labels == None: + labels = cls.load_labels(path) + + # checking if the store is fragmented and compute num_leading_symbols + names = os.listdir(path) + aname = names[0] + apath = os.path.join(path, aname) + if os.path.isdir(apath): + num_leading_symbols = len(aname) + else: + num_leading_symbols = 0 + + # load the store + measurements = [ + Measurement.load(path, label, num_leading_symbols) for label in labels + ] + measurement_dict = {} + for label, measurement in zip(*[labels, measurements]): + measurement_dict[label] = measurement.count_dict + return measurement_dict diff --git a/src/qibo/tomography_RGD/methodsMiFGD_core.py b/src/qibo/tomography_RGD/methodsMiFGD_core.py new file mode 100755 index 0000000000..dcb5a4963c --- /dev/null +++ b/src/qibo/tomography_RGD/methodsMiFGD_core.py @@ -0,0 +1,753 @@ +import pickle +import time + +import numpy as np +from numpy import linalg as LA + +# from mpi4py import MPI +from qiskit.quantum_info import state_fidelity +from scipy.linalg import interpolative as interp + +############################################################ +## Basic sequential projFGD +## XXX WIP +############################################################ + + +class LoopMiFGD: + def __init__(self): + self.method = "MiFGD" + + def Rnd_stateU(self): + """Random initialization of the state density matrix. + + Returns: + ndarray: the generated random initial U for the state density matrix + """ + + print("self.seed = {}".format(self.seed)) + + seed = self.seed + + stateU = [] + + for xx in range(self.Nr): + real_state = np.random.RandomState(seed).randn(self.num_elements) + + imag_state = np.random.RandomState(seed).randn(self.num_elements) + + seed += 1 + + # real_state = np.random.RandomState().randn(self.num_elements) + # imag_state = np.random.RandomState().randn(self.num_elements) + + state = real_state + 1.0j * imag_state + + # state = 1.0 / np.sqrt(self.num_tomography_labels) * state + state_norm = np.linalg.norm(state) + state = state / state_norm + + stateU.append(state) + + stateU = np.array(stateU).T + + self.U0 = stateU + + return stateU + + def est_Estimation_specNrm(self, stateU): + """estimate the spectral norm + + Args: + stateU (ndarray): state vector representing a state + """ + ZZ0 = stateU @ stateU.T.conj() + nZ0_specN = interp.estimate_spectral_norm(ZZ0) + + Amea1 = np.zeros(self.num_labels) + cnt = np.arange(self.num_labels) + for ii, projector, measurement in zip( + *[cnt, self.projector_list, self.measurement_list] + ): + projU = projector.dot(stateU) + trAUU = np.trace(np.dot(stateU.T.conj(), projU)).real + + Amea1[ii] = trAUU - measurement + + AAz = self.A_dagger_InputZ(Amea1) + nAA_specN = interp.estimate_spectral_norm(AAz) + + DeNom_SN = 11 / 10 * nZ0_specN + nAA_specN + etaSpecN = 1 / (4 * DeNom_SN) + + self.AmeaZZ0 = Amea1 + + self.etaSpecN = etaSpecN + self.nZ0_specN = nZ0_specN + self.nAA_specN = nAA_specN + self.DeNom_SN = DeNom_SN + + def eta_Estimation(self, stateU): + ZZ0 = stateU @ stateU.T.conj() + nZ0 = LA.norm(ZZ0) + + Amea1 = np.zeros(self.num_labels) + # Amea2 = np.zeros(self.num_labels) + cnt = np.arange(self.num_labels) + for ii, projector, measurement in zip( + *[cnt, self.projector_list, self.measurement_list] + ): + projU = projector.dot(stateU) + trAUU = np.trace(np.dot(stateU.T.conj(), projU)).real + + Amea1[ii] = trAUU - measurement + + AAz = self.A_dagger_InputZ(Amea1) + nAA = LA.norm(AAz) + + DeNom = 11 / 10 * nZ0 + nAA + etaTh = 1 / (4 * DeNom) + + self.AmeaZZ0 = Amea1 + + self.etaTh = etaTh + self.nZ0 = nZ0 + self.nAA = nAA + self.DeNom = DeNom + + def A_dagger_InputZ(self, InputZ): + """A^+(y)""" + YY = np.zeros((self.num_elements, self.num_elements)) + + for zz, proj in zip(InputZ, self.projector_list): + YY += zz * proj.matrix + + return YY + + def A_dagger_y(self): + """A^+(y), y is the measurement input""" + YY = np.zeros((self.num_elements, self.num_elements)) + + for yP, proj in zip(self.measurement_list, self.projector_list): + YY += yP * proj.matrix + + return YY + + def ProjPSD(self, Rho): + val, eigV = np.linalg.eig(Rho) + + if np.allclose(val.imag, 0): + val = val.real + + # self.val = val + # self.eigV = eigV + + # X0_reconstruct = eigV @ np.diag(val) @ eigV.T.conj() + # if np.allclose(X0_reconstruct, self.X0): + # print(' the Hermitian decomposition is corect') + # else: + # print(' sth wrong in the Hermitian decomposition') + + pos = val > 0 + posID = np.arange(self.num_elements)[pos] + + # -------------- project to PSD ------------------------- # + Valpos = val[posID] # positive eigen value + posEV = eigV[:, posID] # corresponding eigV with val > 0 + + sortID = np.argsort(Valpos)[::-1] # from large to small + ID_Nr = sortID[: self.Nr] # only keep the largest Nr elem + + Val_Nr = Valpos[ID_Nr] # Nr positive eig-values + EV_Nr = posEV[:, ID_Nr] # the corresponding Eig Vec + + # sqrVal = np.sqrt(Valpos) # keep all positive + # U0 = posEV @ np.diag(sqrVal) # keep all positive + # X1 = posEV @ np.diag(Valpos) @ posEV.T.conj() # keep all positive + # X2 = U0 @ U0.T.conj() # keep all positive + + coef = np.sqrt(10 / 11) # np.sqrt(1/L_hat) + + sqrVal = np.sqrt(Val_Nr) # keep only Nr elem + + U0 = EV_Nr @ np.diag(sqrVal * coef) # keep only Nr elem + # X1 = EV_Nr @ np.diag(Val_Nr) @ EV_Nr.T.conj() # X1 should = X2 + # X2 = U0 @ U0.T.conj() # X1 should = X2 + + # self.sortID = sortID + # self.ID_Nr = ID_Nr + + self.Val_Nr = Val_Nr # Nr positive Eig Value + self.Valpos = Valpos # all positive Eig Value + # self.posEV = posEV # all positive Eig vector + # self.EV_Nr = EV_Nr # Nr positive Eig vector + self.U0 = np.array(U0) + + return np.array(U0) + + def initialize(self, stateU): + + self.stateU = stateU + self.momU = stateU + self.momUdag = stateU.T.conj() + # self.XkU = stateU @ self.momUdag + + # print(' stateU = \n{}'.format(stateU)) + # print(' momUdag = \n{}'.format(self.momUdag)) + + # self.state = state + # self.momentum_state = state[:] + + # self.Xk = np.outer(state, state.T.conj()) + X0 = np.dot(stateU, stateU.T.conj()) + + self.X0 = X0 + + self.Xk = X0 + + # print('real_state = {}'.format(real_state)) + # print('imag_state = {}'.format(imag_state)) + # print('num_tomography_labels = {}'.format(self.num_tomography_labels)) + # print('state_norm = {}'.format(state_norm)) + + def Load_Init(self, Ld): + """to load state vector decomposition from file for MiFGD initialization + + Args: + Ld (str): specification how to load the data from file + + Returns: + ndarray: D * rank matrix representing state vector + """ + + F_X0 = "{}/X0.pickle".format(self.meas_path) + print(" F_X0 = {}".format(F_X0)) + print(" Ld = {}".format(Ld)) + + with open(F_X0, "rb") as f: + w_X0 = pickle.load(f) + + # print(' w_X0 = {}'.format(w_X0)) + + if Ld == "RGD": + u0, v0, s0, X0 = w_X0[Ld] + + stateU = u0 + for ii in range(self.Nr): + scale = np.sqrt(s0[ii, ii]) + stateU[:, ii] = scale * u0[:, ii] + + X0_reconstruct = stateU @ stateU.T.conj() + print( + " X0 - X0_reconstruct = {}\n".format( + np.linalg.norm(X0 - X0_reconstruct) + ) + ) + + else: + U0, X0 = w_X0[Ld] + stateU = U0 + + return stateU + + def compute(self, InitX=0, Ld=0): + """do the MiFGD optimization method + + Basic workflow of gradient descent iteration. + 1. randomly initializes state dnesity matrix. + 2. makes a step (defined differently for each "Worker" class below) until + convergence criterion is satisfied. + + Args: + InitX (int, optional): choice of different initialization. Defaults to 0. + Ld (str, optional): specification how to load the data from file. Defaults to 0. + + """ + + self.InitX = InitX + self.step_Time = {} + self.Init_Time = {} + + tt1 = time.time() + + if InitX == 0: + stateU = self.Rnd_stateU() + + elif InitX == 1: + Adgr_y = self.A_dagger_y() + stateU = self.ProjPSD(Adgr_y) + + elif InitX == -1: + stateU = self.Load_Init(Ld) + + # print(' stateU = {}'.format(stateU)) + tt2 = time.time() + + if self.Option == 1: + self.eta_Estimation(stateU) + self.eta = self.etaTh + elif self.Option == 2: + self.est_Estimation_specNrm(stateU) + self.eta = self.etaSpecN + + tt3 = time.time() + self.initialize(stateU) + + tt4 = time.time() + self.step_Time[-1] = tt4 - tt1 + + self.Init_Time["X0"] = tt2 - tt1 + self.Init_Time["eta"] = tt3 - tt2 + self.Init_Time["momU"] = tt4 - tt3 + self.Init_Time["Tini"] = tt4 - tt1 + + print(" -------- X0 --> (-1)-th iteration [ InitX = {} ] \n".format(InitX)) + print(" X0 step (MiFGD) --> time = {}\n".format(tt4 - tt1)) + + X0_target_Err = np.linalg.norm(self.Xk - self.target_DM, "fro") + self.Target_Err_Xk.append(X0_target_Err) # for X0 + self.InitErr = X0_target_Err + + print( + " X0 --> Tr(X0) = {}\n".format( + np.trace(self.X0) + ) + ) + print(" X0 --> Target Err = {}\n".format(X0_target_Err)) + print(" MiFGD num_iterations = {}\n".format(self.num_iterations)) + + self.Unorm = [] # to record the norm of state U + + # print(' ****************************************** ') + # print('self.InitX = {}'.format(self.InitX)) + # print('self.Nr = {}'.format(self.Nr)) + # print('self.momU.shape = {}'.format(self.momU.shape)) + # print('self.momUdag.shape = {}'.format(self.momUdag.shape)) + # print('self.stateU.shape = {}'.format(self.stateU.shape)) + # print('self.U0.shape = {}'.format(self.U0.shape)) + + for self.iteration in range(self.num_iterations): + if not self.converged: + self.step() + else: + break + if self.convergence_iteration == 0: + self.convergence_iteration = self.iteration + + def convergence_check(self): + """ + Check whether convergence criterion is satisfied by comparing + the relative error of the current estimate and the target density matrices. + Utilized in "step" function below. + """ + + if ( + self.process_idx == 0 + and self.iteration % self.convergence_check_period == 0 + ): + # compute relative error + + # numerator = density_matrix_diff_norm(self.state, self.previous_state) + # denominator = density_matrix_norm(self.state) + # error = numerator + # relative_error = numerator / denominator + + Run_state_err = 0 + if self.Nr == 1 and Run_state_err == 1: + # error = density_matrix_diff_norm(self.state, self.previous_state) + # relative_error = error / density_matrix_norm(self.state) + error = density_matrix_diff_norm(self.stateU, self.previous_stateU) + relative_error = error / density_matrix_norm(self.stateU) + + self.error_list.append(error) + + # relative_errU = np.linalg.norm(self.stateU - self.previous_stateU) / np.linalg.norm(self.previous_stateU) + xx = np.linalg.norm( + Col_flip_sign(self.stateU) - Col_flip_sign(self.previous_stateU) + ) + relative_errU = xx / np.linalg.norm(self.previous_stateU) + + # self.relative_error_list.append(relative_error) + # self.Err_relative_st.append(relative_error) + self.Err_relative_st.append(relative_errU) + + XkErrRatio = np.linalg.norm(self.Xk - self.Xk_old) / np.linalg.norm( + self.Xk_old + ) + self.Err_relative_Xk.append(XkErrRatio) + + # print(' || Xk - Xk(old) ||_F = {}'.format(np.linalg.norm(self.Xk - self.Xk_old))) + # print(' | psi - pis(old) | = {}'.format(np.linalg.norm(self.stateU - self.previous_stateU))) + + if self.target_DM is not None: # adding for checking the target error + + ## from LA.norm(DM_Wst - rho.full()) need two matrices + + # Xk = np.outer(self.state, self.state.T.conj()) + # self.Xk = Xk + + numerator2 = np.linalg.norm(self.Xk - self.target_DM) + + fidelity_DM = state_fidelity(self.Xk, self.target_DM, validate=False) + + self.Target_Err_Xk.append(numerator2) + self.fidelity_Xk_list.append(fidelity_DM) + + print( + " relative_errU = {}, relative_errX = {}".format( + relative_errU, XkErrRatio + ) + ) + print(" Target_Error = {}\n".format(numerator2)) + + # if relative_error <= self.relative_error_tolerance: + # if relative_errU <= self.relative_error_tolerance: + if min(relative_errU, XkErrRatio) <= self.relative_error_tolerance: + self.converged = True + self.convergence_iteration = self.iteration + + if self.target_state is not None: + + ## from density_matrix_diff_norm(Wst, target_St) need two states + # numerator1 = density_matrix_diff_norm(self.state, self.target_state) + # self.FrobErr_st.append(numerator1) # = self.target_error_list + + Xk_Err_compare = 0 + if Xk_Err_compare == 1: + # if self.Nr == 1: + # ----------------- # + # original MiFGD # + # ----------------- # + + # compute target relative error (numerator extremely slow) + numerator = density_matrix_diff_norm( + self.stateU, self.target_state + ) # extremely slow + denominator = density_matrix_norm(self.target_state) + + target_error = numerator + target_relative_error = numerator / denominator + self.target_error_list.append(target_error) # = self.Target_Err_Xk + self.target_relative_error_list.append(target_relative_error) + + ### check: if NOT validate=False then this fails + fidelity = state_fidelity( + self.target_state, self.stateU, validate=False + ) + + self.fidelity_list.append(fidelity) + + +# import methods_GetParam +# import methodsRGD +# from methodsRGD import WorkerRGD +# from methods_GetParam import WorkerParm +from methods_ParmBasic import BasicParameterInfo + + +# class BasicWorker(WorkerParm, LoopMiFGD): +class BasicWorker(BasicParameterInfo, LoopMiFGD): + """ + Basic worker class. Implements MiFGD, which performs + accelerated gradient descent on the factored space U + (where UU^* = rho = density matrix) + """ + + def __init__(self, params_dict, params_MiFGD): + """initialization step for optimzation: to load all the sampled Pauli + operators and measurement results + + Args: + params_dict (dict): dictionary of parameters + """ + + eta = params_MiFGD["eta"] # for MiFGD + mu = params_MiFGD.get("mu", 0.0) + Option = params_MiFGD.get("Option", 0) + + self.eta = eta # step size / learning rate (for MiFGD) + self.mu = mu # MiFGD momentum parameter (large: more acceleration) + self.Option = Option + + # WorkerParm.__init__(self, + # process_idx, + # num_processes, + # params_dict, input_S) + # params_dict) + + BasicParameterInfo.__init__(self, params_dict) + + LoopMiFGD.__init__(self) + + @staticmethod + def single_projection_diff(projector, measurement, state): + r""" + Computes gradient of the L2-norm objective function. + - Objective function: f(u) = 0.5 * \sum_i ( - y_i)^2 + - Gradient of f(u) wrt u: grad(u) = \sum_i ( - y_i) * A_i * u := \sum_i grad_i (u) + + Explanation of the variables below: + - projection: A_i * u term --> grad_i(u) = (trace(projection u*) - y_i) * projection + - trace = np.dot(projection, state.conj()) = trace(projection u*) + - diff = (trace - measurement) * projection = grad_i(u) + """ + projection = projector.dot(state) + trace = np.dot(projection, state.conj()) + diff = (trace - measurement) * projection + return diff + + @staticmethod + def single_projU_diff(projector, measurement, stateU, Udag): + r""" + Computes gradient of the L2-norm objective function. + - Objective function: f(u) = 0.5 * \sum_i ( - y_i)^2 + - Gradient of f(u) wrt u: grad(u) = \sum_i ( - y_i) * A_i * u := \sum_i grad_i (u) + + Explanation of the variables below: + - projection: A_i * u term --> grad_i(u) = (trace(projection u*) - y_i) * projection + - trace = np.dot(projection, state.conj()) = trace(projection u*) + - diff = (trace - measurement) * projection = grad_i(u) + + stateU = self.momU, i.e. the momentum state + Udag = self.momU.T.conj() = self.momUdag + """ + # projection = projector.dot(state) + # trace = np.dot(projection, state.conj()) + # diff = (trace - measurement) * projection + + projU = projector.dot(stateU) + # trAUU = np.trace(np.dot(projU, Udag)) + trAUU = np.trace(np.dot(Udag, projU)).real + + diffU = (trAUU - measurement) * projU + + # print(' ------------------------ ') + # print('projection = {}'.format(projection)) + # print('projection.shape = {}'.format(projection.shape)) + # print('state = {}'.format(state)) + # print('state.conj() = {}'.format(state.conj())) + # print('state.shape = {}'.format(state.shape)) + + # print('trace = {}'.format(trace)) + # print('measurement= {}, trace - measurement = {}'.format(measurement, trace - measurement)) + # print('diff = {}'.format(diff)) + # print(' =============== ') + # print('trAUU = {}'.format(trAUU)) + # print('projU =\n{}'.format(projU)) + # print('diffU =\n{}'.format(diffU)) + + return diffU + + def projection_diff(self): + + # state_diff = np.zeros(self.num_elements, dtype=complex) + # stateU_Diff = np.zeros((self.num_elements, self.Nr), dtype=complex) + stateU_Diff = np.zeros(self.stateU.shape, dtype=complex) + + for projector, measurement in zip( + *[self.projector_list, self.measurement_list] + ): + # state_diff += self.single_projection_diff(projector, measurement, self.momentum_state) + + # td1 = time.time() + diffU = self.single_projU_diff( + projector, measurement, self.momU, self.momUdag + ) + + # print(' stateU_Diff = {}'.format(stateU_Diff)) + # print(' diffU = {}'.format(diffU)) + # print(' stateU_Diff.shape = {}'.format(stateU_Diff.shape)) + # print(' diffU.shape = {}'.format(diffU.shape)) + + stateU_Diff += diffU + # state_diff += diff + # print(' state_diff = \n {}'.format(state_diff)) + + # td2 = time.time() + # print(' diffU --> time = {}'.format(td2-td1)) + + # print(' state_diff =\n{}'.format(state_diff)) + # print(' stateU_Diff =\n{}'.format(stateU_Diff)) + + # xx = stateU_Diff.T.conj() @ stateU_Diff + + return stateU_Diff + + def step(self): + + # self.XkU_old = self.XkU + self.previous_stateU = self.stateU + self.previous_momU = self.momU + self.Xk_old = self.Xk # added by Ming-Chien + + print(" -------- {}-th iteration \n".format(self.iteration)) + tt1 = time.time() + + # self.previous_state = self.state[:] + # self.previous_momentum_state = self.momentum_state[:] + + stateU_Diff = self.projection_diff() + tt2 = time.time() + + # state = self.momentum_state - self.eta * state_diff + stateU = self.momU - self.eta * stateU_Diff + + # print(' {}-th ieration'.format(self.iteration)) + # print(' stateU_Diff = \n {}'.format(stateU_Diff)) + + self.Unorm.append(np.linalg.norm(stateU)) # to record the norm of state U + + # self.state = clip_normalize(state, self.trace) + # self.momentum_state = self.state + self.mu * (self.state - self.previous_state) + + self.stateU = clip_normalize(stateU, self.trace) + self.momU = self.stateU + self.mu * (self.stateU - self.previous_stateU) + self.momUdag = self.momU.T.conj() + + tt3 = time.time() + + if self.iteration % 100 == 0: + print(self.iteration) + + # print(' self.process_idx = {}'.format(self.process_idx)) + # print(' self.iteration = {}'.format(self.iteration)) + # print(' self.convergence_check_period = {}'.format(self.convergence_check_period)) + + # self.Xk = np.outer(self.state, self.state.T.conj()) + self.Xk = self.stateU @ self.stateU.T.conj() + + tt4 = time.time() + + self.convergence_check() + tt5 = time.time() + + print(" projection_diff() --> time = {}".format(tt2 - tt1)) + print(" update variabels --> time = {}".format(tt3 - tt2)) + print(" calc Xk --> time = {}".format(tt4 - tt3)) + print(" convergence_check --> time = {}".format(tt5 - tt4)) + print(" step (MiFGD) --> time = {}\n".format(tt5 - tt1)) + + self.step_Time[self.iteration] = tt5 - tt1 + + +############################################################ +## Utility functions +## XXX To modularize/package +############################################################ + + +def density_matrix_norm(state): + conj_state = state.conj() + norm = np.sqrt( + sum([v**2 for v in [np.linalg.norm(state * item) for item in conj_state]]) + ) + return norm + + +def density_matrix_diff_norm(xstate, ystate): + conj_xstate = xstate.conj() + conj_ystate = ystate.conj() + + norm = np.sqrt( + sum( + [ + v**2 + for v in [ + np.linalg.norm(xstate * xitem - ystate * yitem) + for xitem, yitem in zip(*[conj_xstate, conj_ystate]) + ] + ] + ) + ) + return norm + + +def clip_normalize(vector, threshold): + norm = np.linalg.norm(vector) + if norm > threshold: + vector = (threshold / norm) * vector + return vector + + +## ------------------------------------------------ ## +## MC personal writing for check ## +## i.e. written by Ming-Chien ## +## ------------------------------------------------ ## + + +def Col_flip_sign(xCol): + # ID_max_abs = np.argmax(np.abs( xCol)) + # sign = np.sign(xCol[ID_max_abs]) + + # print(' ----------------------------------------------- ') + + if xCol.shape[0] < xCol.shape[1]: + print(" *** ERROR: This is a row, not a column ***") + return + + ID_max_abs = [np.argmax(np.abs(xCol[:, ii])) for ii in range(xCol.shape[1])] + sign = np.sign([xCol[ID_max_abs[ii], ii] for ii in range(xCol.shape[1])]) + + # print(' xCol.shape = {}'.format(xCol.shape)) + # print(' sign.shape = {}'.format(sign.shape)) + # print(' sign = {}'.format(sign)) + + # print(' xCol = {}'.format(xCol[-5:])) + # xCol = sign * xCol # may have diemsion problem + xCol = np.multiply(sign, xCol) + # print(' xCol = {}'.format(xCol[-5:])) + # print(' ***** ') + + return xCol + + +def test_check_norm(state): + """similar to density_matrix_norm""" + + conj_state = state.conj() + norm = np.sqrt( + sum([v**2 for v in [np.linalg.norm(state * item) for item in conj_state]]) + ) + + xx = [item for item in conj_state] + yy = [state * item for item in conj_state] + zz = [np.linalg.norm(state * item) for item in conj_state] + + print(state) + print(xx) + print(yy) + print(zz) + + return norm + + +def test_diff(xstate, ystate): + """this is similar to def density_matrix_diff_norm(xstate, ystate):""" + conj_xstate = xstate.conj() + conj_ystate = ystate.conj() + + norm = np.sqrt( + sum( + [ + v**2 + for v in [ + np.linalg.norm(xstate * xitem - ystate * yitem) + for xitem, yitem in zip(*[conj_xstate, conj_ystate]) + ] + ] + ) + ) + + xx = [(xitem, yitem) for xitem, yitem in zip(*[conj_xstate, conj_ystate])] + + yy = [ + np.linalg.norm(xstate * xitem - ystate * yitem) + for xitem, yitem in zip(*[conj_xstate, conj_ystate]) + ] + + print(xx) + print(yy) + print(xstate) + print(ystate) + + return norm diff --git a/src/qibo/tomography_RGD/methodsRGD_core.py b/src/qibo/tomography_RGD/methodsRGD_core.py new file mode 100755 index 0000000000..ef1488c194 --- /dev/null +++ b/src/qibo/tomography_RGD/methodsRGD_core.py @@ -0,0 +1,1662 @@ +import pickle + +# import scipy.sparse as sparse +import time +from functools import reduce + +import numpy as np +import projectors +import scipy.sparse as sparse +from numpy import linalg as LA + +# from mpi4py import MPI +from qiskit.quantum_info import state_fidelity +from scipy.sparse.linalg import svds + +# import measurements + + +# --------------------------------- # +# the origianl RGD package # +# --------------------------------- # + + +############################################################ +## Basic sequential RGD +## XXX WIP +############################################################ + + +class LoopRGD: + def __init__(self): + self.method = "RGD" + + def initialize_RndSt(self): + """ + Random initialization of the state density matrix. + """ + + print("\n ****** initial choice same as the MiFGD ***** \n") + print("self.seed = {}".format(self.seed)) + self.InitChoice = "random initial (same as MiFGD)" + + seed = self.seed + + stateIni = [] + for xx in range(self.Nr): + real_state = np.random.RandomState(seed).randn(self.num_elements) + + # seed += 1 + imag_state = np.random.RandomState(seed).randn(self.num_elements) + + seed += 1 + + # real_state = np.random.RandomState().randn(self.num_elements) + # imag_state = np.random.RandomState().randn(self.num_elements) + + state = real_state + 1.0j * imag_state + + # state = 1.0 / np.sqrt(self.num_tomography_labels) * state + state_norm = np.linalg.norm(state) + state = state / state_norm + + stateIni.append(state) + + stateIni = np.array(stateIni).T + + self.state = stateIni + self.uk = stateIni + self.vk = stateIni + self.ukh = stateIni.T.conj() + self.vkh = np.transpose(stateIni.conj()) + + self.s0_choice = 0 + if self.s0_choice == 0: + # ss = np.random.random(self.Nr) + # ss = ss / np.sum(ss) + ss = np.ones(self.Nr) + + elif self.s0_choice == 1: + ss = np.ones(self.Nr) / self.Nr + + self.sDiag = np.diag(ss) + + X0 = stateIni @ self.sDiag @ self.vkh + + self.u0 = stateIni + self.v0 = stateIni + self.s0 = np.diag(ss) + self.X0 = X0 + + self.Xk = X0 + + self.check_Hermitian_x_y(self.uk, self.vk) + + def initialize_yAd(self): + """initialize the initial density matrix for the algorithm""" + + # print('\n ****** initial choice following paper choice ***** ') + # self.Id = np.eye(self.num_elements, dtype=complex) ## matrix dimension d = num_elements = 2**Nk + self.InitChoice = "paper" + + # ------------------------------------------------------------ # + + if self.Ch_svd >= 0: # need to compute X0 + + print(" start running self.A_dagger") + tt1 = time.time() + # y = self.yIni ## this y = coef * measurement_list -> order follows self.label_list + + if self.Md_tr == 0: # the normal def of A, i.e. sampling all Pauli + Xk = self.A_dagger(self.yIni) # 1st quickest + + elif self.Md_tr == 1 or self.Md_tr == 2: # add constraint on Identity + Xk = self.A_dagger_tr1(self.projector_list, self.yIni, self.yI0) + + tt2 = time.time() + + print(" self.A_dagger done --> time = {}".format(tt2 - tt1)) + print(" Initial SVD --> choice = {}".format(self.Ch_svd)) + + uk, sDiag, vkh = Hr_Hermitian( + Xk, self.Nr, self.Ch_svd + ) # Ch_svd =0: full SVD + + elif self.Ch_svd < 0: # don't need to compute X0 + + print( + " ********* using randomized-SVD to construct X0 = uk @ sDiag @ vkh **************************" + ) + + tt2a = time.time() + uk, sDiag, vkh = self.rSVD(k=self.Nr, s=2, p=15) + tt2b = time.time() + print(" self.rSVD --> time = {}".format(tt2b - tt2a)) + + # Compare_2_SVD(u1, sDiag1, v1h, uk, sDiag, vkh) + + self.check_Hermitian_x_y(uk, vkh.T.conj()) + + if self.EigV_positive == 1: # U == V + vk = uk + vkh = uk.T.conj() + + else: # U != V + vk = vkh.T.conj() + + ukh = uk.T.conj() + Xk = uk @ sDiag @ vkh # only keep rank r singular vectors + + self.u0 = uk + self.v0 = vk + self.s0 = sDiag + self.X0 = Xk + + self.Xk = Xk + self.uk = uk + self.vk = vk + + self.ukh = ukh + self.vkh = vkh + self.sDiag = sDiag + + def A_dagger_vec(self, vec): + """to obtain A^+(y) @ vec, where y is the input vector for A^dagger operator + recorded in self.yIni + + Args: + vec (ndarray): a vector which is supposed to be the left singular vector + + Returns: + ndarray: Xu = the A^+(y) @ vec + """ + + Xu = np.zeros((self.num_elements, vec.shape[1]), dtype=complex) + for yP, proj in zip(self.yIni, self.projector_list): + Xu += yP * proj.dot(vec) + Xu *= self.coef + + return Xu + + def A_dagger_ym_vec(self, zm, vec): + """to obtain A^+(zm) @ vec + + Args: + zm (ndarray): the input vector for the A^+ operator + vec (ndarray): a vector which is supposed to be the left singular vector + + Returns: + ndarray: Xu = the A^+(y) @ vec + """ + + Xu = np.zeros((self.num_elements, vec.shape[1]), dtype=complex) + for yP, proj in zip(zm, self.projector_list): + Xu += yP * proj.dot(vec) + # Xu *= self.coef + + return Xu + + def rSVD(self, k, s=3, p=12, matrixM_Hermitian=1): + """randomized SVD + + ref: https://arxiv.org/pdf/1810.06860.pdf --> A ~ QQ* A (B = Q* A ) + with modification: if Remain_Hermitian --> A ~ QQ* A QQ* (B = Q* A Q) + + Args: + k (int): = Nr # rank parameter + s (int, optional): specify oversampling. Defaults to 3. + p (int, optional): specify power parameter. Defaults to 12. + matrixM_Hermitian (int, optional): specify the matrix is Hermitian or not. Defaults to 1. + + Returns: + ndarray: (u) left singular vectors + ndarray: np.diag(s) the diagonal matrix with diagonal elements being s + ndarray: (vh) complex conjugate of the right singular vectors + """ + + qDs = min(k + s, self.num_elements) # k + s small dimension for Q + + Omega = np.random.RandomState().randn(self.num_elements, qDs) + + # Mw = M @ Omega + # Mw = self.A_dagger_vec(Omega) + Mw = self.A_dagger_ym_vec(self.yIni, Omega) + + Q, R = LA.qr(Mw) + + for ii in range(p): + tt1 = time.time() + + ATQ = self.A_dagger_ym_vec(self.yIni, Q) + + G, R = LA.qr(ATQ) + tt2 = time.time() + + AG = self.A_dagger_ym_vec(self.yIni, G) + + Q, R = LA.qr(AG) + + tt3 = time.time() + print( + " *** {}-th (A A*) done: Time --> ATQ: {}, AG: {} ***".format( + ii, tt2 - tt1, tt3 - tt2 + ) + ) + + if matrixM_Hermitian == 0: + # B = Q.T.conj() @ M + B = self.A_dagger_vec(Q).T.conj() + + uB, s, vh = LA.svd(B) + u = Q @ uB[:, :k] # k = Nr + s = s[:k] # k = Nr + vh = vh[:k, :] # k = Nr + + elif matrixM_Hermitian == 1: + # B = Q.T.conj() @ M @ Q + B = Q.T.conj() @ self.A_dagger_vec(Q) + + uB, s, vh = LA.svd(B, hermitian=True) + u = Q @ uB[:, :k] # k = Nr + s = s[:k] # k = Nr + + vh = vh[:k, :] @ Q.T.conj() # k = Nr + + return u, np.diag(s), vh + + def check_Hermitian_x_y(self, x, y, Hermitian_criteria=1e-13): + """to check wheter x and y are the same, + where x and y are supposed to be the left and right singular vectors + if x = y, then the original SVDed matrix is Hermitian + + Args: + x (ndarray): the 1st vector to compare, supposed to be the left singular vector + y (ndarray): the 2nd vector to compare, supposed to be the right singular vector + Hermitian_criteria (float, optional): the numerical error minimum criteria. Defaults to 1e-13. + """ + + # if LA.norm(x - y) < Hermitian_criteria: + if np.allclose(x, y): + + self.Remain_Hermitian = 1 # True for remaining Hermitian + self.EigV_positive = 1 + + # elif LA.norm(Col_flip_sign(x) - Col_flip_sign(y)) < Hermitian_criteria: + elif np.allclose(Col_flip_sign(x), Col_flip_sign(y)): + self.Remain_Hermitian = 1 + self.EigV_positive = 0 + + else: + self.EigV_positive = None + self.Remain_Hermitian = 0 # False for remaining Hermitian + self.st_Non_Hermitian.append( + self.iteration + ) # start num for non-Hermitian + + self.EigV_pm_List.append(self.EigV_positive) + self.Hermitian_List.append(self.Remain_Hermitian) + + print( + " Hermitian ? Remain_Hermiain = {}, EigV_postive = {}".format( + self.Remain_Hermitian, self.EigV_positive + ) + ) + + def Get_measured_y(self, Md_tr=0): + """to get the coefficients of all sampled Pauli operators for the A^+ operator + + Args: + Md_tr (int, optional): Method if including trace = 1 or not. Defaults to 0. + """ + + if Md_tr == 0: + coef = np.sqrt(self.num_elements / self.num_labels) + self.yIni = np.array(self.measurement_list) * coef + self.coef = coef + + elif Md_tr == 1 or Md_tr == 2: + Iden = "".join(["I" for i in range(self.n)]) + self.label_list = self.label_list + [Iden] # No effect in compute + + projI = projectors.Projector(Iden) + self.projI = projI + + dd = self.num_elements + coef = np.sqrt((dd**2 - 1) / (dd * self.num_labels)) + + self.yml_sc = coef # scale for ym from measurement_list + self.yIni = np.array(self.measurement_list) * coef + self.yI0 = np.sqrt(1 / self.num_elements) + + if Md_tr == 2: # weighted sum + self.w0 = 2 + self.wm = np.ones(self.num_labels) + self.wm_sqrt = np.ones(self.num_labels) # each element is squred rooted + + def Load_Init(self, Ld): + """to load state vector decomposition (U, s, V) from file + for the RGD optimization initialization + + Args: + Ld (str): specification how to load the data from file + + """ + + F_X0 = "{}/X0.pickle".format(self.meas_path) + with open(F_X0, "rb") as f: + w_X0 = pickle.load(f) + + if Ld == "RGD": + u0, v0, sDiag, X0 = w_X0[Ld] + + else: + U0, X0 = w_X0[Ld] # from 'MiFGD' + + self.Xk = X0 + if self.Nr == 1: + self.uk = U0 + self.vk = U0 + self.ukh = U0.T.conj() + self.vkh = U0.T.conj() + self.sDiag = np.array([[1.0]]) + + else: + print(" Not Implemented Yet") + + self.check_Hermitian_x_y(self.uk, self.vk) + + self.X0 = X0 + + def computeRGD(self, InitX, Ch_svd=0, Md_tr=0, Md_alp=0, Md_sig=0, Ld=0): + """Basic workflow of gradient descent iteration. + + 1. initializes state dnesity matrix. + 2. mapkes a step (defined differently for each "Worker" class below) until + convergence criterion is satisfied. + + Args: + InitX (int): choice of the initial matrix + Ch_svd (int, optional): _description_. Defaults to 0. + Md_tr (int, optional): Method if including trace = 1. Defaults to 0. + Md_tr = 0, the usual RGD algorithm, i.e. sampling all S + = 1, to enforce coef for Identiy, i.e. not sampling Identity + Md_alp (int, optional): method for scaling alpha. Defaults to 0. + Md_sig (int, optional): method for scaling singular value. Defaults to 0. + Ld (str, optional): specification how to load the data from file. Defaults to 0. + """ + # self.Id = np.eye(self.num_elements, dtype=complex) ## matrix dimension d = num_elements = 2**Nk + + self.InitX = InitX + self.Md_tr = Md_tr # method of including identity operator or not + + self.Md_alp = Md_alp # method for scaling alpha + self.Md_sig = Md_sig # method for scaling sinular values + self.Ch_svd = Ch_svd # choice for initial SVD + + self.EigV_pm_List = ( + [] + ) # Eig-value is positive or not (=1: positive, = 0, non-positive, = None, no EigV) + self.Hermitian_List = [] + self.st_Non_Hermitian = [] + self.step_Time = {} + + tt1 = time.time() + + self.iteration = -1 + + self.Get_measured_y(Md_tr) + print(" self.coef = {}".format(self.coef)) + print(" InitX = {}".format(InitX)) + + if InitX == 0: + self.initialize_RndSt() # initial by random + elif InitX == 1: + self.initialize_yAd() # initial by A^\dagger(y) + elif InitX == -1: + self.Load_Init(Ld) + + tt2 = time.time() + self.step_Time[-1] = tt2 - tt1 + + print( + " -------- X0 --> (-1)-th iteration [ InitX = {}, SVD choice = {} ] \n".format( + InitX, Ch_svd + ) + ) + print(" X0 step (RGD) --> time = {}\n".format(tt2 - tt1)) + + X0_target_Err = LA.norm(self.Xk - self.target_DM, "fro") + self.Target_Err_Xk.append(X0_target_Err) # for X0 + self.InitErr = X0_target_Err + + print( + " X0 --> Tr(X0) = {}\n".format( + np.trace(self.X0) + ) + ) + print(" X0 --> Target Err = {}\n".format(X0_target_Err)) + print(" RGD max num_iterations = {}\n".format(self.num_iterations)) + + self.uGv_list = [] + self.Rec_Alpha = [] + self.Alpha_sc = [] + + self.sDiag_list = [] + self.zm_list = [] # = ym - Axk + self.z0_list = [] # = yI0 - A0 + + for self.iteration in range(self.num_iterations): + if not self.converged: + self.stepRGD() + else: + break + if self.convergence_iteration == 0: + self.convergence_iteration = self.iteration + + def convergence_check(self): + """ + Check whether convergence criterion is satisfied by comparing + the relative error of the current estimate and the target density matrices. + Utilized in "step" function below. + """ + if ( + self.process_idx == 0 + and self.iteration % self.convergence_check_period == 0 + ): + # compute relative error + + XkErrRatio = LA.norm(self.Xk - self.Xk_old) / LA.norm(self.Xk_old) + + xx = LA.norm(Col_flip_sign(self.uk) - Col_flip_sign(self.uk_old)) + + ukErrRatio = xx / LA.norm(self.uk_old) + + print(" min(uk +/- uk_old) = {}\n".format(xx)) + + # self.Rec_st_Err.append( LA.norm(self.uk - self.uk_old) ) + self.Err_relative_Xk.append(XkErrRatio) + + self.Err_relative_st.append(ukErrRatio) + + if ( + min(ukErrRatio, XkErrRatio) <= self.relative_error_tolerance + ): # using state or Xk to check convergence + self.converged = True + self.convergence_iteration = self.iteration + print( + " ********* XkErrRatio = {} < StpTol ******".format(XkErrRatio) + ) + + if self.target_DM is not None: + Fro_diff = LA.norm(self.Xk - self.target_DM, "fro") + + if np.isnan(Fro_diff): + print( + " ********* {}-th Fro_diff = NaN = {}".format( + self.iteration, Fro_diff + ) + ) + # break + return + + fidelity_DM = state_fidelity(self.Xk, self.target_DM, validate=False) + + self.Target_Err_Xk.append(Fro_diff) + self.fidelity_Xk_list.append(fidelity_DM) + + print( + " relative_errU = {}, relative_errX = {}".format( + ukErrRatio, XkErrRatio + ) + ) + print(" Target_Error = {}\n".format(Fro_diff)) + + if Fro_diff > 1e2: + raise ValueError(" Target_Err_Xk too big !!! Stop and check \n") + + +from methods_ParmBasic import BasicParameterInfo + + +class BasicWorkerRGD(LoopRGD): + """ + Basic worker class. Implements MiFGD, which performs + accelerated gradient descent on the factored space U + (where UU^* = rho = density matrix) + """ + + def __init__(self, params_dict): + """the initialization of setting up the parameters for the optimizer + + Args: + params_dict (dict): dictionary of parameters + """ + + BasicParameterInfo.__init__(self, params_dict) + + LoopRGD.__init__(self) + + def Amea_selfP(self, XX): + """do the sampling operator A(.) on the input matrix XX + + Args: + XX (ndarray): the input matrix + + Returns: + ndarray: the result of the sampling operator A(XX) + """ + + yMea = np.zeros(self.num_labels) + for ii, proj in enumerate(self.projector_list): + ProjM = proj.matrix + data = ProjM.data + col = ProjM.col + row = ProjM.row + yMea[ii] = np.dot(data, np.asarray(XX[col, row]).reshape(-1)).real + + return self.coef * yMea + + def Amea(self, proj_list, XX): + """the implementation of the sampling operator A + + Args: + proj_list (list): list of Pauli matrix operators + XX (ndarray): the input matrix + + Returns: + ndarray: the final result of the sampling operator A + """ + + yMea = [ + np.dot( + proj_list[ii].matrix.data, + np.asarray( + XX[proj_list[ii].matrix.col, proj_list[ii].matrix.row] + ).reshape(-1), + ).real + for ii in range(len(proj_list)) + ] # correct + + return np.sqrt(self.num_elements / self.num_labels) * np.array(yMea) + + def Amea_tr1(self, proj_list, XX): + """to get the coefficients of the sampled Pauli operator including the identity matrix + in the basis expansion of density matrix XX + + Args: + proj_list (list): list of all sampled Pauli operators + supposed to be worker.projector_list + XX (ndarray): matrix representing the density matrix + + Returns: + ndarray: (yml) array representing the coefficients for each sampled Pauli operator + in the basis expansion of the density matrix XX + float : (y0) coefficient of the identity in the basis expansion + of the density matrix XX + """ + + yMea = [ + np.dot( + proj_list[ii].matrix.data, + np.asarray( + XX[proj_list[ii].matrix.col, proj_list[ii].matrix.row] + ).reshape(-1), + ).real + for ii in range(self.num_labels) + ] # correct + yml = self.yml_sc * np.array(yMea) + + y0 = np.trace(XX).real / np.sqrt(self.num_elements) + + return yml, y0 + + def A_dagger(self, ym): + """the implementation of A^+ (A dagger) operator + + Args: + ym (ndarray): the input vector as the coefficient array for the Pauli matrices + + Returns: + ndarray: the matrix output of A_dagger operator + """ + YY = np.zeros((self.num_elements, self.num_elements)) + + for yP, proj in zip(ym, self.projector_list): + YY += yP * proj.matrix + + return self.coef * YY + + def A_dagger_tr1(self, proj_list, ym, y0): + """the implementation of A^+ (A dagger) operator, + similar to the function A_dagger + but with the identity matrix inherently included, + i.e. adding the constraint for Identity + + Args: + proj_list (list): list of Pauli operator matrices + ym (ndarray): the input vector as the coefficient array for the Pauli matrices + y0 (float): the cofficient in front of the identity matrix + + Returns: + ndarray: the matrix output of A_dagger operator + """ + # fastest method + YY = np.zeros(proj_list[0].matrix.shape) + + for ii in range(self.num_labels): + YY += ym[ii] * proj_list[ii].matrix + + coef = self.yml_sc + + Y0 = self.projI.matrix * (y0 / np.sqrt(self.num_elements)) + + return coef * YY + Y0 + + def Pt(self, Gk): + """projection to the tangent space spanned by uk and vk, where + uk and vk are the left and right singular vectors of the current predction self.Xk + + Args: + Gk (ndarray): a matrix to be projected onto the tangent space + + Returns: + ndarray: (PtG) the projected matrix of the input matrix Gk onto the tangent space + ndarray: (uGv) uk @ Gk @ vk + """ + + uG = self.ukh @ Gk + Gv = Gk @ self.vk + uGv = uG @ self.vk + + PtG = self.uk @ uG + Gv @ self.vkh - self.uk @ (uGv @ self.vkh) + + self.Gv = Gv + self.Gu = uG.T.conj() # suppose G is Hermitian + + return PtG, uGv + + def Hr_tangentWk(self, Alpha, uGv): + """Hard thresholding Hr(.) of the intermediate matrix Jk + according to the 4th step in the RGD algorithm in the paper, + and update the SVD of the new predicted self.Xk = Hr(Jk) + + Args: + Alpha (float): the step size in the 2nd step of the RGD algorithm + uGv (ndarray): uk @ Gk @ vk + + Returns: + ndarray: (uk) updated left singular vectors of self.Xk + ndarray: (sDiag) updated singular values of self.Xk + ndarray: (vk) updated right singular vectors of self.Xk + """ + + Nr = self.Nr + + # ------------------------------------------------- # + # re-scaling Alpha according to uGv or sDiag # + # ------------------------------------------------- # + max_scaling_time = 1 + for ii_sc in range(max_scaling_time): + print(" ************ {}-th scaling for the Alpha".format(ii_sc)) + + D2s = Alpha * uGv + self.sDiag + + if self.EigV_positive == 1: # Hermitian & all EigVal > 0 + Y2 = Alpha * (self.Gu - self.uk @ uGv) + + q2, r2 = LA.qr(Y2) + # print("k={}, (q2.shape, r2.shape) = ({}, {})".format(k, q2.shape, r2.shape)) + U2r = np.concatenate((self.uk[:, :Nr], q2[:, :Nr]), axis=1) + # print("U2r shape = {}".format(U2r.shape)) + + D2s = np.concatenate((D2s, r2[:Nr, :Nr].T.conj()), axis=1) + # print(D2s.shape) + + else: # not Hermitian OR Eig-val < 0, i.e. U != V + Y1 = Alpha * (self.Gu - self.vk @ uGv.T.conj()) + Y2 = Alpha * (self.Gv - self.uk @ uGv) + + q1, r1 = LA.qr(Y1) + # print("k={}, (q1.shape, r1.shape) = ({}, {})".format(k, q1.shape, r1.shape)) + q2, r2 = LA.qr(Y2) + # print("k={}, (q2.shape, r2.shape) = ({}, {})".format(k, q2.shape, r2.shape)) + U2r = np.concatenate((self.uk[:, :Nr], q2[:, :Nr]), axis=1) + # print("U2r shape = {}".format(U2r.shape)) + V2r = np.concatenate((self.vk[:, :Nr], q1[:, :Nr]), axis=1) + # print("V2r shape = {}".format(V2r.shape)) + + D2s = np.concatenate((D2s, r1[:Nr, :Nr].T.conj()), axis=1) + # print(D2s.shape) + + D2s = np.concatenate( + (D2s, np.concatenate((r2[:Nr, :Nr], np.zeros((Nr, Nr))), axis=1)), + axis=0, + ) + # print(D2s.shape) + + print( + " Hr_tangentWk --> Remain_Hermitian = {}, EigV_positve = {}".format( + self.Remain_Hermitian, self.EigV_positive + ) + ) + print( + " D2s - D2s.T.conj() = {}".format(LA.norm(D2s - D2s.T.conj())) + ) + print( + " np.allclose(D2s, D2s.T.conj()) = {}\n".format( + np.allclose(D2s, D2s.T.conj()) + ) + ) + + if np.allclose(D2s, D2s.T.conj()) == True: + Mu2r, s2r, Mv2rh = LA.svd(D2s, full_matrices=True, hermitian=True) + + else: # not Hermitian + Mu2r, s2r, Mv2rh = LA.svd(D2s, full_matrices=True) + + Mu2r_Nr = Mu2r[:, :Nr] + Mv2r_Nr = Mv2rh[:Nr, :].T.conj() + + print(" s2r = {}".format(s2r)) + print(" max(s2r) = {}".format(max(s2r))) + if max(s2r) < 1: + break + else: + Alpha = Alpha * 0.5 + + self.Alpha_sc.append(ii_sc) + self.Rec_Alpha.append(Alpha) + print( + " ------------ DONE svd of D2s with {}-th scaling (#max sc={})".format( + ii_sc, max_scaling_time + ) + ) + + # --------------------------------------------------------- # + # END of re-scaling Alpha according to uGv or sDiag # + # --------------------------------------------------------- # + + sDiag = np.diag(s2r[:Nr]) + + if self.EigV_positive == 1: # original U = V + + uk = U2r.dot(Mu2r_Nr) + vk = U2r.dot(Mv2r_Nr) # since may Mu2r_Nr != Mv2r_Nr + + else: # original U != V + + uk = U2r.dot(Mu2r_Nr) + vk = V2r.dot(Mv2r_Nr) + + self.check_Hermitian_x_y(uk, vk) + print( + " error of Hermitian = LA.norm(|uk| - |vk|) = {}".format( + LA.norm(Col_flip_sign(uk) - Col_flip_sign(vk)) + ) + ) + + return uk, sDiag, vk + + def calc_GkV_UGk(self, zm): + """calculate the Gk, PtG, and uGv needed for the RGD algorithm + + Args: + zm (ndarray): the input float array for the A^+ + corresponding to coefficients of all sampled Pauli operators + + Returns: + ndarray: (Gk) the matrix from the 1st step of the RGD algorithm + ndarray: (PtG) the Gk projected onto the tangent space + ndarray: (uGv) uk @ Gk @ vk + """ + + YY = np.zeros(self.projector_list[0].matrix.shape) + + for ii in range(self.num_labels): + YY += zm[ii] * self.projector_list[ii].matrix + + coef = np.sqrt(self.num_elements / self.num_labels) + + Gk = coef * YY + + uG = self.ukh @ Gk + Gv = Gk @ self.vk + uGv = uG @ self.vk + + PtG = self.uk @ uG + Gv @ self.vkh - self.uk @ (uGv @ self.vkh) + + self.Gv = Gv + self.Gu = uG.T.conj() # suppose G is Hermitian + + return Gk, PtG, uGv + + def single_projUV_diff(self, measurement, proj): + + if self.EigV_positive == 1: # U == V + + # Uproj = proj.dot(self.uk).T.conj() + projU = proj.dot(self.uk) + + zmP = measurement - np.trace(self.sDiag @ self.vkh @ projU).real # quikcer + + zm_projU = zmP * projU + + return zm_projU + + else: # U != V + + projU = proj.dot(self.uk) + + projV = proj.dot(self.vk) + UprojV = self.ukh @ projV + + zm_projV = zmP * projV + + zm_projU = zmP * projU + zm_UpV = zmP * UprojV + + return zm_projU, zm_projV, zm_UpV + + def single_projUV_diff_zmP_UV(self, proj, zmP): + """to calculate zmP * proj @ uk + and zmP * proj @ vk + + Args: + proj (class): the given Pauli operator object + zmP (float): the coefficient corresponding to the given proj + needed as input for the A^+ operator + + Returns: + ndarray: (zm_projU) zmP * proj @ uk + ndarray: (zm_projV) zmP * proj @ vk + """ + + projU = proj.dot(self.uk) + projV = proj.dot(self.vk) + + zm_projV = zmP * projV + zm_projU = zmP * projU + + return zm_projU, zm_projV + + @staticmethod + def single_projUV_diff_zmP_Hermitian(proj, uk): + """go obtain proj @ uk + + Args: + proj (class): the given Pauli operator object + uk (ndarray): the left singular vector of self.Xk + + Returns: + ndarray: (projU) proj @ uk + """ + + projU = proj.dot(uk) + + return projU + + def calc_PtG_2_uG_Gv_Hermitian(self): + """to calculate uk @ Gk @ vk + and updated the projection of Gk onto the tangent space + + Returns: + ndarray: (uGv) uk @ Gk @ vk + """ + + Gu = np.zeros((self.num_elements, self.Nr), dtype=complex) + + for proj, zmP in zip(*[self.projector_list, self.zm]): + + projU = self.single_projUV_diff_zmP_Hermitian(proj, self.uk) + Gu += zmP * projU + + self.Gu = self.coef * Gu + uGv = self.ukh @ self.Gu + + self.PtGk = ( + self.uk @ self.Gu.T.conj() + self.Gu @ self.ukh - self.uk @ uGv @ self.ukh + ) + + return uGv + + def calc_PtG_2_uG_Gv(self): + """to project Gk onto the tangnet space and + calculate the related objects + + Returns: + ndarray: (uGv) uk@ Gk @ vk + """ + + Gu = np.zeros((self.num_elements, self.Nr), dtype=complex) + + if self.EigV_positive == 1: # U = V + + Have_Amea = 1 + if Have_Amea == 1: + for proj, zmP in zip(*[self.projector_list, self.zm]): + + projU = self.single_projUV_diff_zmP_Hermitian(proj, self.uk) + Gu += zmP * projU + + self.Gu = self.coef * Gu + uGv = self.ukh @ self.Gu + + self.PtGk = ( + self.uk @ self.Gu.T.conj() + + self.Gu @ self.ukh + - self.uk @ uGv @ self.ukh + ) + + elif Have_Amea == 0: + + for measurement, proj in zip( + self.measurement_list, self.projector_list + ): + + zm_projU = self.single_projUV_diff(measurement, proj) + + Gu += zm_projU + + self.Gu = (self.num_elements / self.num_labels) * Gu + uGv = self.ukh @ self.Gu + + self.PtGk = ( + self.uk @ self.Gu.T.conj() + + self.Gu @ self.ukh + - self.uk @ uGv @ self.ukh + ) + + else: # U != V + Gv = np.zeros(self.vk.shape, dtype=complex) + + for zmP, proj in zip(self.zm, self.projector_list): + + zm_projU, zm_projV = self.single_projUV_diff_zmP_UV(proj, zmP) + + Gu += zm_projU + Gv += zm_projV + + print( + " np.allclose( Col_flip_sign(Gu), Col_flip_sign(Gv) ) = {}".format( + np.allclose(Col_flip_sign(Gu), Col_flip_sign(Gv)) + ) + ) + + self.Gv = self.coef * Gv + self.Gu = self.coef * Gu + uGv = self.ukh @ self.Gv + + self.PtGk = ( + self.uk @ self.Gu.T.conj() + + self.Gv @ self.vkh + - self.uk @ uGv @ self.vkh + ) + + return uGv + + def calc_n1_n2_direct_Gk(self, zm, z0): + """calculate the necessary matrices in the RGD algorithm + + Args: + zm (ndarray): coefficient array for the sampled Pauli matrices in the sampling operator A + z0 (float): coefficient for the identify operator + + Returns: + ndarray: (Gk) the matrix obtained in the 1st step of the RGD algorithm + ndarray: (uGv) u @ Gk @ v + ndarray: (PtGk) Gk projected onto the tangent space + """ + + # ------------------------------------------------- # + # calc of Gk = A^+ (zm), where zm = y-Axk # + # ------------------------------------------------- # + tt1 = time.time() + + if self.Md_tr == 0: + Gk = self.A_dagger(zm) + + elif self.Md_tr == 1: + Gk = self.A_dagger_tr1(self.projector_list, zm, z0) + elif self.Md_tr == 2: + zm = zm * self.wm # element-wise multiplication + z0 = z0 * self.w0 + Gk = self.A_dagger_tr1(self.projector_list, zm, z0) + + tt2 = time.time() + + # --------------------------------- # + # PtGk = P(Gk) # + # --------------------------------- # + + PtGk, uGv = self.Pt(Gk) + + self.PtGk = PtGk + + tt3 = time.time() + + return Gk, uGv, PtGk + + def calc_n1_n2(self): + # ----------------------------- # + # calc AptGk = A(PtGk) # + # ----------------------------- # + tt1 = time.time() + + if self.Md_tr == 0: + AptGk = Amea( + self.projector_list, self.PtGk, self.num_labels, self.coef + ) # quickest + + n2 = LA.norm(AptGk) + # print(n2) + + elif self.Md_tr == 1: + AptGk, A0 = self.Amea_tr1(self.projector_list, self.PtGk) + n2 = np.sqrt(LA.norm(AptGk) ** 2 + A0**2) + + elif self.Md_tr == 2: + AptGk, A0 = self.Amea_tr1(self.projector_list, self.PtGk) + AptGk = AptGk * self.wm_sqrt + + n2 = np.sqrt( + LA.norm(AptGk) ** 2 + A0**2 * self.w0 + ) # weighted sum in the inner product + + tt2 = time.time() + + n1 = LA.norm(self.PtGk) + # print(n1) + + print(" [calc_n1_n2] AptGk --> time = {}\n".format(tt2 - tt1)) + + # --------------------------------- # + # calculate (n1, n2) # + # --------------------------------- # + + if n1 == 0.0: + print("Projected Gradient PtGk norm = n1 = 0 = {}".format(n1)) + print(" --> should achieve the minimum | or not successful ??") + return + + self.n1 = n1 # not necessary to record + self.n2 = n2 # not necessary to record + + Alpha0 = (n1 / n2) ** 2 + + if self.Md_alp == 0: + Alpha = Alpha0 + + elif self.Md_alp == 1: + Alpha = min(Alpha0, 1) + + elif self.Md_alp == 2: + Alpha = min(Alpha0, 2) + + elif self.Md_alp == 3: + Alpha = min(0.5 * Alpha0, 0.8) + + return Alpha + + def zm_from_Amea(self): + """the sampling operator A(.)""" + + if self.Md_tr == 0: + Axk = self.Amea_selfP(self.Xk) ## 1st quickest + self.z0 = 0 + elif self.Md_tr == 1 or self.Md_tr == 2: + Axk, A0 = self.Amea_tr1(self.projector_list, self.Xk) + + self.z0 = self.yI0 - A0 + self.z0_list.append(z0) + + self.zm = ( + self.yIni - Axk + ) ## [yIni = coef*measurement_list] order follows self.label_list + + self.zm_list.append(LA.norm(self.zm)) + + def stepRGD(self): + """each iteration step of the RGD algorithm""" + + self.Xk_old = self.Xk + self.uk_old = self.uk + + print(" -------- {}-th iteration \n".format(self.iteration)) + tt1 = time.time() + + # ----------------------------------------- # + # calc of Axk = mapA(Xk) # + # ----------------------------------------- # + + self.zm_from_Amea() + + tt2a = time.time() + + # ----------------------------------------------------- # + # calculate (n1, n2) by direct calculation of Gk # + # ----------------------------------------------------- # + + print( + " {}-th step -> self.Remain_Hermitian = {}, self.EigV_positive = {}".format( + self.iteration, self.Remain_Hermitian, self.EigV_positive + ) + ) + + if self.EigV_positive == 1: # Hermitian & EigV > 0 --> U = V + + uGv = self.calc_PtG_2_uG_Gv_Hermitian() + + else: # U != V + + uGv = self.calc_PtG_2_uG_Gv() + + tp2b = time.time() + + print(" calc_PtG_2_uG_Gv --> time = {}".format(tp2b - tt2a)) + + Alpha = self.calc_n1_n2() + + uk, sDiag, vk = self.Hr_tangentWk(Alpha, uGv) + + # --------------------------------------------------------- # + # SOME approaches to scale the singular values or not # + # --------------------------------------------------------- # + + if self.Md_sig == 0: + # ratio = 1 + self.sDiag = sDiag + + else: + ss_Ary = np.diag(sDiag) + + if self.Md_sig == 1: + ratio = 1 / np.sum(ss_Ary) # scaling of the sDiag + elif self.Md_sig == 2: + ratio = max(0.9, min(1 / np.sum(ss_Ary), 1.1)) # scaling of the sDiag + elif self.Md_sig == 3: + if np.sum(ss_Ary) > 1: # similar to MiFGD case + ratio = 1.0 / np.sum(ss_Ary) + else: + ratio = 1.0 + + self.sDiag = ratio * sDiag + + # ------------------------------------- # + # update the Xk, uk, vk # + # ------------------------------------- # + self.uk = uk + self.ukh = np.transpose(np.conj(uk)) + + if self.EigV_positive == 1: # U == V + self.vkh = np.transpose(np.conj(uk)) + self.vk = uk + + else: + self.vkh = np.transpose(np.conj(vk)) + self.vk = vk + + self.Xk = self.uk.dot(self.sDiag) @ self.vkh + + self.uGv_list.append(uGv) + + self.sDiag_list.append(np.diag(self.sDiag)) + + if self.iteration % 100 == 0: + print(" **** {}-th iteratation\n".format(self.iteration)) + + tt7 = time.time() + self.convergence_check() + tt8 = time.time() + + print(" convergence check --> time = {}\n".format(tt8 - tt7)) + print(" stepRGD --> time = {}\n".format(tt8 - tt1)) + + self.step_Time[self.iteration] = tt8 - tt1 + + +############################################################ +## Utility functions +## XXX To modularize/package +############################################################ + + +def density_matrix_norm(state): + """create density matrix from state + + Args: + state (ndarray): state vector + + Returns: + float: the norm of the density matrix + """ + conj_state = state.conj() + norm = np.sqrt( + sum([v**2 for v in [np.linalg.norm(state * item) for item in conj_state]]) + ) + return norm + + +def density_matrix_diff_norm(xstate, ystate): + """compare the difference between the density matrices constructed from xstate and ystate + + Args: + xstate (ndarray): 1st state vector + ystate (ndarray): 2nd state vector + + Returns: + float: the norm of the matrix difference + """ + conj_xstate = xstate.conj() + conj_ystate = ystate.conj() + + norm = np.sqrt( + sum( + [ + v**2 + for v in [ + np.linalg.norm(xstate * xitem - ystate * yitem) + for xitem, yitem in zip(*[conj_xstate, conj_ystate]) + ] + ] + ) + ) + return norm + + +# ========================================= # +# specifically for RGD # +# ========================================= # + + +# -------------------------------------------------------------------- # +# this one satisfy our unit convention # +# -------------------------------------------------------------------- # + + +def Amea(proj_list, XX, m, coef): + """the implementation of the sampling operator A + + Args: + proj_list (list): list of Pauli matrix operators + XX (ndarray): the input matrix + m (int): = self.num_labels = the number of sampled Pauli matrices + coef (float): the value of scaling for the final result + + Returns: + ndarray: the final result of the sampling operator A + """ + + yMea = np.zeros(m) + for ii, proj in enumerate(proj_list): + ProjM = proj.matrix + yMea[ii] = np.dot( + ProjM.data, np.asarray(XX[ProjM.col, ProjM.row]).reshape(-1) + ).real + + return coef * yMea + + +def A_dagger(proj_list, ym, m, Nk): + """the implementation of A^+ (A dagger) operator + + Args: + proj_list (list): list of Pauli operator matrices + ym (ndarray): the input vector as the coefficient array for the Pauli matrices + m (int): the number of sampled Pauli operators + Nk (int): the value of qubit number + + Returns: + ndarray: the matrix output of A_dagger operator + """ + + method = 0 + + # fastest method + if method == 0: + YY = np.zeros(proj_list[0].matrix.shape) + + for ii in range(m): + YY += ym[ii] * proj_list[ii].matrix + + return np.sqrt(2**Nk) / np.sqrt(m) * YY + + +def Compare_2_SVD(u1, s1, v1h, u2, s2, v2h): + """compare two SVD results and see if they are equal or not + + Args: + u1 (ndarray): the left singular vector of the 1st SVD result + s1 (ndarray): the diagnoal matrix of the singular values of the 1st SVD result + v1h (ndarray): the complex conjugate of the right singular vector of the 1st SVD result + u2 (ndarray): the left singular vector of the 2nd SVD result + s2 (ndarray): the diagnoal matrix of the singular values of the 2nd SVD result + v2h (ndarray): the complex conjugate of the right singular vector of the 2nd SVD result + """ + + print(" s1 = {}, s2 = {}, diff = {}".format(s1, s2, s1 - s2)) + print( + " np.allclose(u1, u2) = {}".format( + np.allclose(Col_flip_sign(u1), Col_flip_sign(u2)) + ) + ) + + print( + " np.allclose(vkh, v5h) = {}".format( + np.allclose(Row_flip_sign(v1h), Row_flip_sign(v2h)) + ) + ) + print( + " np.allclose(vk, v5) = {}".format( + np.allclose(Col_flip_sign(v1h.T.conj()), Col_flip_sign(v2h.T.conj())) + ) + ) + + print( + " np.allclose(xR1, xR2) = {}".format( + np.allclose(u1 @ s1 @ v1h, u2 @ s2 @ v2h) + ) + ) + + print(" u1 - u2 = {}".format(LA.norm(u1 - u2))) + print(" v1h - v2h = {}".format(LA.norm(v1h - v2h))) + print(" XR1 - XR2 = {}".format(LA.norm(u1 @ s1 @ v1h - u2 @ s2 @ v2h))) + print(" --------------------------------------------------------------------- ") + + +def rSVD(M, k, s=5, p=5): + """randomized SVD + + k = Nr # rank parameter + s = 5 # oversampling + p = 5 # power parameter + + ref: https://arxiv.org/pdf/1810.06860.pdf --> A ~ QQ* A (B = Q* A ) + with modification: if Remain_Hermitian --> A ~ QQ* A QQ* (B = Q* A Q) + """ + qDs = min(k + s, M.shape[1]) # k + s small dimension for Q + + Omega = np.random.RandomState().randn(M.shape[1], qDs) + + Mw = M @ Omega + Q, R = LA.qr(Mw) + + for ii in range(p): + ATQ = M.T.conj() @ Q + G, R = LA.qr(ATQ) + + AG = M @ G + Q, R = LA.qr(AG) + + print(" ****** {}-th ATQ & AG done *****".format(ii)) + + Remain_Hermitian = 1 + if Remain_Hermitian == 0: + B = Q.T.conj() @ M + + uB, s, vh = LA.svd(B) + u = Q @ uB[:, :k] # k = Nr + s = s[:k] # k = Nr + vh = vh[:k, :] # k = Nr + + elif Remain_Hermitian == 1: + B = Q.T.conj() @ M @ Q + + uB, s, vh = LA.svd(B, hermitian=True) + u = Q @ uB[:, :k] # k = Nr + s = s[:k] # k = Nr + + vh = vh[:k, :] @ Q.T.conj() # k = Nr + + return u, np.diag(s), vh + + +def Row_flip_sign(yRow): + """to change the sign of each row + such that the largest element of each row is positive. + + Args: + yRow (ndarray): the input row vector + + Returns: + ndarray: the updated yRow vector + """ + + if yRow.shape[0] > yRow.shape[1]: + print(" *** ERROR: This is a colum, not a row ***") + return + + ID_max_abs1 = np.argmax(np.abs(yRow), axis=1) + ID_max_abs2 = [np.argmax(np.abs(yRow[ii, :])) for ii in range(yRow.shape[0])] + sign = np.sign([yRow[ii, ID_max_abs1[ii]] for ii in range(yRow.shape[0])]) + + yRow = [sn * yr for sn, yr in zip(sign, yRow)] + yRow = np.array(yRow) + + return yRow + + +def Col_flip_sign(xCol): + """to change the sign of each column + such that the largest element of each column is positive. + + The purpose of this function is for comparison, such as that will be + used in check_Hermitian_x_y + + Args: + xCol (ndarray): the input column vector + + Returns: + ndarray: the update xCol with sign changed + """ + + if xCol.shape[0] < xCol.shape[1]: + print(" *** ERROR: This is a row, not a column ***") + return + + ID_max_abs = [np.argmax(np.abs(xCol[:, ii])) for ii in range(xCol.shape[1])] + sign = np.sign([xCol[ID_max_abs[ii], ii] for ii in range(xCol.shape[1])]) + + xCol = np.multiply(sign, xCol) + + return xCol + + +def power_Largest_EigV(M, criteria=1e-15, seed=0): + """use the power method to obtain the largest eigenvalue and eigenvector + + Args: + M (ndarray): the input matrix + criteria (float, optional): stopping criteria of the power method. + Defaults to 1e-15. + seed (int, optional): random seed parameter. Defaults to 0. + + if converged: + Returns: + float: (lam_pseudo) the largest eigenvalue + ndarray: (Mv) the eigen vector of the largest eigenvalue + int: (ii) the number of iterations for convergence + """ + + converged = 0 + InitV = np.ones((M.shape[1], 1)) + + for init_cnt in range(5): + InitV = InitV / LA.norm(InitV) + Vinput = InitV + + for ii in range(500): + Mv = M @ Vinput + lam_pseudo = LA.norm(Mv) + Mv /= lam_pseudo + + Mv = Col_flip_sign(Mv) + + diff = LA.norm(Mv - Vinput) + Vinput = Mv + print( + " {}-th iteration: lambda = {}, diff = {}".format(ii, lam_pseudo, diff) + ) + if diff < criteria: + break + + if diff > criteria: + print(" *** power method not converged: diff = {}".format(diff)) + print(" Now {}-th init --> try another InitV \n".format(init_cnt)) + + InitV = np.random.RandomState().randn(M.shape[1], 1) + + else: + print( + " **** Largest EigV obtained from the power method: ||Mv- v|| = {} ***".format( + diff + ) + ) + print( + " (at the {}-th initV; {}-th iteration) \n".format( + init_cnt, ii + ) + ) + converged = 1 + break + + if converged == 0: + return + elif converged == 1: + return lam_pseudo, Mv, ii + + +def Hr_Hermitian(X, r, Choice=0): + """hard theresholding the matrix X to rank r approximation + + Args: + X (ndarray): the input matrix + r (int): the target rank for the final approximated matrix + Choice (int, optional): the method of doing SVD. Defaults to 0. + + Returns: + ndarray: (u) the final left singular vector + ndarray: np.diag(s)[:r, :r] = the matrix with diagonal elements the singular values + ndarray: (vh) the complex conjugate of the right singular vector + """ + + print(" Choice for Hr_Hermitian = {}\n".format(Choice)) + + if Choice == 0: + u, s, vh = LA.svd(X, hermitian=True) + + u = np.array(u[:, :r]) # only keep the first r columns + vh = np.array(vh[:r, :]) # only keep the first r rows + elif Choice == 1: + u, s, vh = svds(X, k=r) + + elif Choice == 2: # only for Nr = r = 1 + s, u, ConvergeCnt = power_Largest_EigV(X) + s = [s] + u = np.array(u) + vh = u.T.conj() + + return u, np.diag(s)[:r, :r], vh + + +def P(u, vh, Gk, rank): + """project the matrix Gk to the tangent space + + Args: + u (ndarray): the left singular vector + vh (ndarray): complex conjugate of the right singular vector + Gk (ndarray): the input matrix + rank (int): the rank of the singular value decomposed matrix + + Returns: + ndarray: the matrix Gk projected to the tangent space + """ + # Gk = Gk + + u = u[:, :rank] + v = np.transpose(np.conj(vh)) + + v = v[:, :rank] + + U = np.matmul(u, np.transpose(np.conj(u))) + V = np.matmul(v, np.transpose(np.conj(v))) + + return np.matmul(U, Gk) + np.matmul(Gk, V) - np.matmul(np.matmul(U, Gk), V) + + +def calc_n1_n2(zm, Ai, uk, vk, ukh, vkh, dd, m): + + # --------------------------------------------------------- # + # < method 2 > go around Gk # + # --------------------------------------------------------- # + + # ---------------------------------------------- # + # to construct a sequence of + # Ei = Ai * V + # Pi = Ai * U + # Pi^+ = U^+ * Ai + # Fi = U^+ * Ai * V + # ---------------------------------------------- # + + Pi = [Ai[kk] @ uk for kk in range(m)] + Ei = [Ai[kk] @ vk for kk in range(m)] + Fi = [vkh @ Ei[kk] for kk in range(m)] + + zPi = [zm[ii] * Pi[ii] for ii in range(m)] + zEi = [zm[ii] * Ei[ii] for ii in range(m)] + zFi = [ukh @ zEi[ii] for ii in range(m)] + + Eis = [Ei[ii].T.conj() for ii in range(m)] ## = Ei^* \in (r, dd) + Fis = [Fi[ii].T.conj() for ii in range(m)] ## = Fi^* = V^* @ G @ U \in (r,r) + + fsum = lambda x, y: x + y + GkU = reduce(fsum, zPi) ## = Gk @ U = sum over zi * Ai @ U \in (dd, r) + GkV = reduce(fsum, zEi) ## = Gk @ V = sum over zi * Ai @ V \in (dd, r) + UsG = GkU.T.conj() ## = U^* @ Gk + + UsGV = reduce(fsum, zFi) ## = U^* @ Gk @ V = sum over zi * U^* @ Ai @ V \in (r,r) + + # --------------------------------------------- # + # calculating Y1t = PvPerp @ Gk @ worker.uk # + # --> need zPi = zi * Ai @ worker.uk # + # --------------------------------------------- # + + Y1t = vkh @ GkU ## \in (r, r) | GkU = xx = reduce(lambda x,y: x+y, zPi) \in (dd, r) + Y1t = GkU - vk @ Y1t ## \in (dd,r) + + # --------------------------------------------- # + # calculating Y2t = PuPerp @ Gk @ worker.vk # + # --> need zEi = zi * Ai @ worker.vk # + # --------------------------------------------- # + + Y2t = ( + ukh @ GkV + ) ## \in (r, r) | GkV = xx = reduce(lambda x,y: x+y, zEi) \in (dd, r) + Y2t = GkV - uk @ Y2t ## \in (dd,r) + + # ----------------------------------------------------- # + # uGv = worker.ukh @ Gk @ worker.vk \in C(r,r) # + # --> need zFi = zi * worker.ukh @ Ai @ worker.vk # + # ----------------------------------------------------- # + + n1 = LA.norm(UsGV) ** 2 + LA.norm(Y1t) ** 2 + LA.norm(Y2t) ** 2 + n1 = np.sqrt(dd / m) * np.sqrt(n1) + + Y1t = np.sqrt(dd / m) * Y1t + Y2t = np.sqrt(dd / m) * Y2t + + # ---------------------------------------------------- # + # (II) to calculate || A(PtGk) ||_2 = worker.n2 # + # where dd = 2^Nk # + # ---------------------------------------------------- # + + PzP = [np.trace(UsG @ Pi[ii]) for ii in range(m)] ## mat \in (r,r) + EzE = [np.trace(Eis[ii] @ GkV) for ii in range(m)] ## mat \in (r, r) + + FzF1 = [np.trace(Fis[ii] @ UsGV) for ii in range(m)] # mat \in (r, r) + AptGk = np.array(PzP) + np.array(EzE) - np.array(FzF1) + + AptGk = (dd / m) * np.array(AptGk).real + + n2 = LA.norm(AptGk) + + return n1, n2, UsGV * np.sqrt(dd / m), Y1t, Y2t diff --git a/src/qibo/tomography_RGD/methods_ParmBasic.py b/src/qibo/tomography_RGD/methods_ParmBasic.py new file mode 100755 index 0000000000..b8b73641b0 --- /dev/null +++ b/src/qibo/tomography_RGD/methods_ParmBasic.py @@ -0,0 +1,76 @@ +# +# get basic parameter information for tomography +# + + +class BasicParameterInfo: + def __init__(self, params_dict): + + # + # read in params_dict + # + Nr = params_dict.get("Nr", 1) + trace = params_dict.get("trace", 1.0) + target_state = params_dict.get("target_state", None) + target_DM = params_dict.get("target_DM") + + label_list = params_dict.get("labels") + measurement_list = params_dict.get("measurement_list") + projector_list = params_dict.get("projector_list") + + num_iterations = params_dict.get("num_iterations") + convergence_check_period = params_dict.get("convergence_check_period", 10) + + relative_error_tolerance = params_dict.get("relative_error_tolerance", 0.001) + seed = params_dict.get("seed", 0) + + # + # basic system information + # + n = len(label_list[0]) + d = 2**n + + self.n = n # number of qubits + self.num_elements = d + self.Nr = Nr # the rank of the target_density_matrix + + self.trace = trace + self.target_state = target_state + self.target_DM = target_DM # target state --> density matrix + + self.num_labels = len(label_list) + self.measurement_list = measurement_list + self.projector_list = projector_list + + # + # numerical book keeping + # + + self.process_idx = 0 + + self.num_iterations = num_iterations + + self.convergence_check_period = ( + convergence_check_period # how often to check convergence + ) + self.relative_error_tolerance = ( + relative_error_tolerance # user-decided relative error + ) + self.seed = seed + + self.Err_relative_st = [] + + self.Target_Err_st = [] # Frobenius norm difference from State + self.Target_Err_Xk = [] # Frobenius norm difference from matrix Xk + + self.target_error_list = [] + self.target_relative_error_list = [] + + self.fidelity_Xk_list = [] # Fidelity between (Xk, target) + self.Err_relative_Xk = [] + + self.iteration = 0 + self.converged = False + self.convergence_iteration = 0 + + self.fidelity_list = [] diff --git a/src/qibo/tomography_RGD/projectors.py b/src/qibo/tomography_RGD/projectors.py new file mode 100755 index 0000000000..ebc210b655 --- /dev/null +++ b/src/qibo/tomography_RGD/projectors.py @@ -0,0 +1,978 @@ +import multiprocessing +import os +import pickle +import time +from functools import reduce +from itertools import product + +import numpy as np +import scipy.sparse as sparse + +# ------------------------------------------- # +# below following MiFGD code # +# ------------------------------------------- # + +# Coordinates of non-zero entries in each of the X, Y, Z Pauli matrices... +ij_dict = { + "I": [(0, 0), (1, 1)], + "X": [(0, 1), (1, 0)], + "Y": [(0, 1), (1, 0)], + "Z": [(0, 0), (1, 1)], +} + + +# ... and the coresponding non-zero entries +values_dict = {"I": [1.0, 1.0], "X": [1.0, 1.0], "Y": [-1.0j, 1.0j], "Z": [1.0, -1.0]} + +# X, Y, Z Pauli matrices +matrix_dict = { + "I": np.array([[1.0, 0.0], [0.0, 1.0]]), + "X": np.array([[0.0, 1.0], [1.0, 0.0]]), + "Y": np.array([[0.0, -1.0j], [1.0j, 0.0]]), + "Z": np.array([[1.0, 0.0], [0.0, -1.0]]), +} +# XXX Actually, from matrix_dict we can generate the above + + +## ---------------------------------- ## +## build projector labels ## +## ---------------------------------- ## + + +def binarize(x): + return int("".join([str(y) for y in x]), 2) + # return ''.join([str(y) for y in x]) + + +def generate_random_label(n, symbols=["I", "X", "Y", "Z"]): + num_symbols = len(symbols) + label = "".join([symbols[i] for i in np.random.randint(0, num_symbols, size=n)]) + return label + + +def generate_random_label_list( + size, n, Rm_Id=0, symbols=["I", "X", "Y", "Z"], factor=1.0, factor_step=0.1 +): + + factor = 1.0 + factor_step = 0.1 + + factor = factor + factor_step + effective_size = int(size * factor) + + ## the essence of 'set' is that 'set does not contain repeated elements' + labels = list({generate_random_label(n, symbols) for i in range(effective_size)}) + + while len(labels) < size: + # print(' *** increasing length ***') + factor = factor + factor_step + effective_size = int(size * factor) + labels = list( + {generate_random_label(n, symbols) for i in range(effective_size)} + ) + + # ----------------------------------------------- # + # to remove Identity controlled by Rm_Id # + # ----------------------------------------------- # + if Rm_Id == 1: + + Iden = "".join(["I" for i in range(n)]) + + Go_Delete = 1 + while Go_Delete == 1: + try: + labels.remove(Iden) + except: + Go_Delete = 0 + + # ------------------------------------- # + # to pad up labels up to num_label # + # ------------------------------------- # + while len(labels) < size: + Add = [generate_random_label(n, symbols)] + if Add != Iden: + labels = labels + Add + + # ----------------------------------------- # + # to keep just enough num_label labels # + # ----------------------------------------- # + labels = labels[:size] + return labels + + +## ------------------------------------------ ## +## constructing the matrix from labels ## +## ------------------------------------------ ## + + +# Generate a projector by accumulating the Kronecker products of Pauli matrices +# XXX Used basically to make sure that our fast implementation is correct +def build_projector_naive(label, label_format="big_endian"): + """to directly generate a Pauli matrix from tensor products + + Args: + label (str): label of the projection, e.g. 'XXZYZ' + label_format (str, optional): the ordering of the label. Defaults to 'big_endian'. + + Raises: + Exception: when the matrix size is too big (i.e. for qubit number > 6) + + Returns: + ndarray: a matrix representing the Pauli operator + """ + if label_format == "little_endian": + label = label[::-1] + if len(label) > 6: + raise Exception("Too big matrix to generate!") + projector = reduce( + lambda acc, item: np.kron(acc, item), + [matrix_dict[letter] for letter in label], + [1], + ) + return projector + + +# Generate a projector by computing non-zero coordinates and their values in the matrix, aka the "fast" implementation +def build_projector_fast(label, label_format="big_endian"): + """to fastly generate a Pauli projection matrix in sparse matrix format + + Args: + label (str): label of the projection, e.g. 'XXZYZ' + label_format (str, optional): the ordering of the label. Defaults to 'big_endian'. + + Returns: + sparse matrix: sparse matrix of the Pauli operator representing label + """ + if label_format == "little_endian": + label = label[::-1] + + n = len(label) + d = 2**n + + # map's result NOT subscriptable in py3, just tried map() -> list(map()) for py2 to py3 + ij = [ + list(map(binarize, y)) + for y in [zip(*x) for x in product(*[ij_dict[letter] for letter in label])] + ] + values = [ + reduce(lambda z, w: z * w, y) + for y in [x for x in product(*[values_dict[letter] for letter in label])] + ] + ijv = list(map(lambda x: (x[0][0], x[0][1], x[1]), zip(ij, values))) + + i_coords, j_coords, entries = zip(*ijv) + + projector = sparse.coo_matrix( + (entries, (i_coords, j_coords)), shape=(d, d), dtype=complex + ) + return projector + + +# XXX Here for redundancy and convenience: has also been added to a separate Projector class + + +# Choose implementation for the projector +def build_projector(label, label_format="big_endian", fast=True): + if fast: + return build_projector_fast(label, label_format) + return build_projector_naive(label, label_format) + + +## --------------------------------- ## +## only save the labels ## +## --------------------------------- ## + + +# utilities for saving in Projector class +def _hdf5_saver(label, path, lock): + lock.acquire() + Projector(label).save(path) + lock.release() + + +def _pickle_saver(label, path): + Projector(label).save(path) + + +def _pickle_saveMap(label): + """to produce the Pauli operator object + + Args: + label (str): Pauli operator label + + Returns: + object: Projector(label) = the class instance of a Pauli operator + """ + return Projector(label) + + +## -------------------------------------------- # +## [testing] classes in projectors # +## -------------------------------------------- # + + +# A projector as a class, hopefully with convenient methods :) +class Projector: + """to create a Pauli projector stored in an efficient way, + instead of writing in the form of matrix directly + + Returns: + class instance: _description_ + """ + + # Generate from a label or build from a dictionary represenation + def __init__(self, arg, label_format="big_endian"): + if isinstance(arg, str): + self.label = arg + self.label_format = label_format + self._generate() + elif isinstance(arg, dict): + data = arg + self._build(data) + + def _build(self, data): + """to build a sparse matrix according to the given data + + Args: + data (dict): specification of the matrix for non-zero values + """ + self.label = data["label"] + self.label_format = data.get("label_format", "big_endian") + + assert data["num_columns"] == data["num_columns"] + entries = data["values"] + i_coords = data["row_indices"] + j_coords = data["column_indices"] + d = data["num_rows"] + dtype = data["value_type"] + + matrix = sparse.coo_matrix( + (entries, (i_coords, j_coords)), shape=(d, d), dtype=complex + ) + self.matrix = matrix + self.csr_matrix = None + + # Here, injecting the "fast" implementation logic for our projector + def _generate(self): + if self.label_format == "little_endian": + label = self.label[::-1] + else: + label = self.label[:] + + n = len(label) + d = 2**n + + # map's result NOT subscriptable in py3, just tried map() -> list(map()) for py2 to py3 + ij = [ + list(map(binarize, y)) + for y in [zip(*x) for x in product(*[ij_dict[letter] for letter in label])] + ] + values = [ + reduce(lambda z, w: z * w, y) + for y in [x for x in product(*[values_dict[letter] for letter in label])] + ] + ijv = list(map(lambda x: (x[0][0], x[0][1], x[1]), zip(ij, values))) + + i_coords, j_coords, entries = zip(*ijv) + matrix = sparse.coo_matrix( + (entries, (i_coords, j_coords)), shape=(d, d), dtype=complex + ) + self.matrix = matrix + self.csr_matrix = None + + # matvec'ing with a vector + def dot(self, x): + return self.csr().dot(x) + + # Get a sparse matrix representation of the projector in CSR format, + # i.e. ideal for matvec'ing it + def csr(self): + if self.csr_matrix is None: + self.csr_matrix = sparse.csr_matrix(self.matrix) + + return self.csr_matrix + + # Get a dict representation of the projector, + # i.e. ideal for serializing it + # XXX Currently with Python's pickle format in mind; moving to json format would add to portability + def dict(self): + """to create a dict representation of the projector + + Returns: + dict: data specifying non-zero values of the matrix + """ + + data = { + "values": self.matrix.data, + "row_indices": self.matrix.row, + "column_indices": self.matrix.col, + "num_rows": self.matrix.shape[0], + "num_columns": self.matrix.shape[1], + "value_type": self.matrix.dtype, + "label": self.label, + } + + return data + + def _pickle_save(self, fpath): + data = self.dict() + with open(fpath, "wb") as f: + pickle.dump(data, f) + + def _hdf5_save(self, fpath): + f = h5py.File(fpath, "a") + group = f.create_group(self.label) + + data_dict = self.dict() + for key in ["column_indices", "row_indices", "values"]: + dataset = group.create_dataset(key, data=data_dict[key]) + group.attrs["num_columns"] = data_dict["num_columns"] + group.attrs["num_rows"] = data_dict["num_rows"] + f.close() + + # Save the projector to disk + def save(self, path): + if os.path.isdir(path): + fpath = os.path.join(path, "%s.pickle" % self.label) + # print("in dir: ", fpath) + self._pickle_save(fpath) + elif path.endswith(".hdf5"): + fpath = path + print("hdf5: ", fpath) + self._hdf5_save(fpath) + + @classmethod + def _pickle_load(cls, fpath): + with open(fpath, "rb") as f: + data = pickle.load(f) + return data + + @classmethod + def _hdf5_load(cls, fpath, label): + + f = h5py.File(fpath, "r") + group = f[label] + + data = {"label": label} + data["num_rows"] = group.attrs["num_rows"] + data["num_columns"] = group.attrs["num_columns"] + + data["column_indices"] = group["column_indices"][:] + data["row_indices"] = group["column_indices"][:] + data["values"] = group["values"][:] + data["value_type"] = data["values"].dtype + return data + + # Load a projector from disk + @classmethod + def load(cls, path, label, num_leading_symbols=0): + """to load the data (dictionary representation of the Pauli projector) + and then create s sparse matrix representation + + Args: + path (str): path storing the Pauli projector + label (str): the label of the Pauli projector + num_leading_symbols (int, optional): the effective number of alphabet in the label. Defaults to 0. + + Returns: + class instance: the object representing the Pauli projector + """ + + if os.path.isdir(path): + if num_leading_symbols == 0: + fpath = os.path.join(path, "%s.pickle" % label) + data = cls._pickle_load(fpath) + + else: + fragment_name = label[:num_leading_symbols] + fpath = os.path.join(path, fragment_name, "%s.pickle" % label) + data = cls._pickle_load(fpath) + + elif path.endswith(".hdf5"): + fpath = path + data = cls._hdf5_load(fpath, label) + + projector = cls(data) + return projector + + +class ProjectorStore: + """to deal with several different Pauli projectors at one time""" + + def __init__(self, labels): + self.labels = labels + self.size = len(labels) + + @classmethod + def combine_proj_bulk(cls, proj_path, method_combine=1): + """to cobmine parallelly produced Pauli operator chunks + + Args: + proj_path (str): the path to store the projectors + method_combine (int, optional): method to do the combination. Defaults to 1. + + Returns: + list: (label_sorted) list of sorted sampled Pauli operator labels + dict: (Pj_combine) the dictionary for all produced Pauli operators + int: (bulk_Pj) number of chunks storing different Pauli projectors + int: (num_cpus) number of cpu for parallel computation + """ + # --------------------------------------------- # + # loading projectors from each bulk # + # --------------------------------------------- # + + F_label_ALL = "{}/ALL_labels.pickle".format(proj_path) + with open(F_label_ALL, "rb") as f: + label_ALL = pickle.load(f) + + proj_lab_files = [ + xx for xx in os.listdir(proj_path) if xx.startswith("labels_") + ] + + bulk_Pj = len(proj_lab_files) + + if method_combine == 0: + print( + " ---------- direct combining Pj_list (NOT parallel) --------------- \n" + ) + + label_combine = [] + Pj_combine = {} + for ii, lab_file in enumerate(proj_lab_files): + td1 = time.time() + + ID_label = int(lab_file.split(".")[0][7:]) + Fname_lab = "{}/{}".format(proj_path, lab_file) + Fname_proj = "{}/Pj_list_{}.pickle".format(proj_path, ID_label) + + ID_Pj, label_Pj, Pj_list = cls.Load_Pj_part( + ID_label, Fname_lab, Fname_proj + ) + + label_combine += label_Pj + Pj_combine.update(Pj_list) + + td2 = time.time() + print( + " --------------- done of updating {}-th Pj from {}".format( + ii, Fname_proj + ) + ) + print( + " ---> Time = {} for {}-th Pj\n".format( + td2 - td1, ii + ) + ) + del Pj_list + del label_Pj + + num_cpus = 1 + + elif method_combine == 1: + + num_cpus = multiprocessing.cpu_count() + if bulk_Pj < num_cpus: + num_cpus = bulk_Pj + print( + " ---------- parallelel #CPU = {} for combining Pj_list -----\n".format( + num_cpus + ) + ) + + pool = multiprocessing.Pool(num_cpus) + + res_list = [] + for lab_file in proj_lab_files: + ID_label = int(lab_file.split(".")[0][7:]) + Fname_lab = "{}/{}".format(proj_path, lab_file) + Fname_proj = "{}/Pj_list_{}.pickle".format(proj_path, ID_label) + + print(" ****** apply_async for ID = {} *****".format(ID_label)) + res = pool.apply_async( + cls.Load_Pj_part_wrap, ([ID_label, Fname_lab, Fname_proj],) + ) + res_list.append(res) + + pool.close() + pool.join() + + print( + " ******* After parallel loading Pj --> len(res_list) = {} ******* \n".format( + len(res_list) + ) + ) + if len(res_list) != bulk_Pj: + print(" ERROR for collecting parallel cpus results \n") + return + + ID_order = [] + label_combine = [] + Pj_combine = {} + + pop_method = 1 + + for ii in range(bulk_Pj): + + if pop_method == 0: + res = res_list[ii] + elif pop_method == 1: + res = res_list.pop(0) + + ID_Pj, label_Pj, Pj_list = res.get() + + print( + " -------- ID = {} is popped from res_list ---------- ".format( + ID_Pj + ) + ) + + ID_order.append(ID_Pj) + label_combine += label_Pj + Pj_combine.update(Pj_list) + print( + " ######## After popping --> len(res_list) = {} ######\n".format( + len(res_list) + ) + ) + + if len(Pj_combine) != len(label_ALL): + print(" ERROR: len(Pj_combine) != len(label_ALL) \n") + return + + label_sorted = sorted(label_combine) + if not label_sorted == label_ALL: + print(" ERROR: label_combine NOT equal label_ALL \n") + return + + if method_combine == 1: + print( + " ID order of returning from parallel CPU = {}\n".format(ID_order) + ) + print( + " ---------- Total # bulk Pj file = {} ---------------".format( + bulk_Pj + ) + ) + print(" ---------- ALL Pj_list are combined --------------- \n") + + return label_sorted, Pj_combine, bulk_Pj, num_cpus + + @classmethod + def Load_Pj_part_wrap(cls, argv): + """wrapper of the function Load_Pj_part + + Args: + argv (tuple): arguments of the function Load_Pj_part + + Returns: + int: (ID_label) the ID number of this Pauli operator chunk + list: (label_Pj) the stored labels + dir: (Pj_list) the stored Pauli prjectors + """ + + ID_label, label_Pj, Pj_list = cls.Load_Pj_part(*argv) + + return ID_label, label_Pj, Pj_list + + @classmethod + def Load_Pj_part(cls, ID_label, Fname_lab, Fname_proj): + """to load labels and the corresponding Pauli operators for this chunk + specified by the ID_label + + Args: + ID_label (int): ID number of this Pauli operator chunk + Fname_lab (str): file name for storing labels + Fname_proj (str): file name for storing the Projectors + + Returns: + int: (ID_label) the ID number of this Pauli operator chunk + list: (label_Pj) the stored labels + dir: (Pj_list) the stored Pauli prjectors + """ + + print( + " Start to load Fname_lab = {} --> ID_label = {}".format( + Fname_lab, ID_label + ) + ) + with open(Fname_lab, "rb") as f: + label_Pj = pickle.load(f) + + with open(Fname_proj, "rb") as f: + Pj_list = pickle.load(f) + print( + " --> ID = {}, Proj_part File = {} is loaded\n".format( + ID_label, Fname_proj + ) + ) + + return ID_label, label_Pj, Pj_list + + # Generate and save the projectors and do it in parallel, + # i.e. using all available cores in your system + def mpPool_map(self, path, bulksize=6000): + format = "hdf5" + if not path.endswith(".hdf5"): + format = "pickle" + if not os.path.exists(path): + os.mkdir(path) + + num_cpus = multiprocessing.cpu_count() + # num_cpus2 = num_cpus - 2 + if num_cpus > len(self.labels): + num_cpus = 3 + # print(' num_cpus = {}, num_cpus2 = {}, self.size ={}'.format(num_cpus, num_cpus2, self.size)) + + pool = multiprocessing.Pool(num_cpus) + # pool = multiprocessing.Pool() + + # --------------------------------------- # + # determine & use label_part # + # --------------------------------------- # + if self.size > bulksize: + method = 0 # = 0 for label_part + + numB = int(np.ceil(self.size / bulksize)) + + label_part = [ + self.labels[ii * bulksize : (ii + 1) * bulksize] for ii in range(numB) + ] + + Partition_Pj = 1 + print(" nB ={}, len(label_part) = {}".format(numB, len(label_part))) + print(" --> Partion_Pj = {}".format(Partition_Pj)) + else: + method = 1 + Partition_Pj = 0 + print(" Partion_Pj = {} --> No partion in Pj".format(Partition_Pj)) + + # ----------------------------------------- # + # use label_part sequentially # + # each part is using parallel CPUs # + # ----------------------------------------- # + + saveP = 1 # the default size of saved bulk Pj_list + if method == 0: + mp_num_max = bulksize * 5 + + mp_method = "map (bulksize sequentially)" + + res = [] + labels_collect = [] + NowSize = 0 + saveP = 1 + for ii, labels in enumerate(label_part): + res_part = pool.map(_pickle_saveMap, labels) + res += res_part + labels_collect += labels + + NowSize += len(labels) + print( + " *** Proj {}-th bulk: # {} projectors were created".format( + ii, NowSize + ) + ) + + if int(NowSize / mp_num_max) >= saveP: + pool.close() + pool.join() + + self.Save_label_Pj(res, labels_collect, path, saveP) + + print( + " *** {}-th labels_part -> saved bulk size = {}, NowSize = {}".format( + ii, len(labels_collect), NowSize + ) + ) + print(" ######### restart pool #########\n") + + saveP += 1 + res = [] + labels_collect = [] + pool = multiprocessing.Pool(num_cpus) + + print(" len(the Final labels_collect) = {}\n".format(len(labels_collect))) + if len(labels_collect) != 0: + self.Save_label_Pj(res, labels_collect, path, saveP) + else: + saveP = saveP - 1 + print("\n saved # bulk = {}, NowSize = {}\n".format(saveP, NowSize)) + + if NowSize != len(self.labels): + print(" the pool map NOT completed yet!\n") + return + + label_ALL_file = "{}/ALL_labels.pickle".format(path) + with open(label_ALL_file, "wb") as f: + pickle.dump(sorted(self.labels), f) + + # ----------------------------------------- # + # the whole label_list = self.labells # + # --> the default size of saveP = 1 # + # ----------------------------------------- # + if method == 1: + mp_method = "map" + res = pool.map(_pickle_saveMap, self.labels) + + elif method == 2: + mp_method = "map_async" + output = pool.map_async(_pickle_saveMap, self.labels) + res = output.get() + + elif method == 3: + mp_method = "apply_async" + + res = [] + for ii in range(self.size): + out = pool.apply_async(_pickle_saveMap, (self.labels[ii],)) + res.append(out.get()) + + pool.close() + pool.join() + + # --------------------------------- # + # saving calculated projectors # + # --------------------------------- # + if method > 0: + if len(res) != len(self.labels): + print(" the pool map NOT completed yet!\n") + return + self.Save_label_Pj(res, self.labels, path) + + print(" pool.{} COMPLETED by #CPU = {} \n".format(mp_method, num_cpus)) + print( + " [projectors] num_cpus = {}, saved bulk Pj = {} \n".format( + num_cpus, saveP + ) + ) + + return num_cpus, saveP, Partition_Pj + + @classmethod + def Save_label_Pj_dict(cls, Pj_list, labels, path, Name=None): + """to save the given Pauli operators and labels in path + + Args: + Pj_list (dict): dictionary referring to all Pauli operators + labels (list): list of Pauli operator labels + path (str): the path storing the Pauli operators + Name (str, optional): suffix for the file name. Defaults to None. + """ + + # ------------------------- # + # saving projectors # + # ------------------------- # + + if Name == None: + Pj_file = "{}/Pj_list.pickle".format(path) + else: + Pj_file = "{}/Pj_list_{}.pickle".format(path, Name) + + with open(Pj_file, "wb") as f: + pickle.dump(Pj_list, f) + print("\n *** Pj_file = {} is saved (i.e. dump)".format(Pj_file)) + + # ------------------------- # + # saving sorted labels # + # ------------------------- # + s_label = sorted(labels) + + if Name == None: + label_file = "{}/labels.pickle".format(path) + else: + label_file = "{}/labels_{}.pickle".format(path, Name) + + with open(label_file, "wb") as f: + pickle.dump(s_label, f) + print(" *** label_file = {} is saved\n".format(label_file)) + + def Save_label_Pj(self, projectors, labels, path, Name=None): + """to save generated Pauli operators + + Args: + projectors (class ProjectorStore): the object representing the projectors + labels (list): list of sampled Pauli operator labels + path (str): the path directory for storing the Pauli operators + Name (str, optional): File suffix. Defaults to None. + """ + + Pj_list = {} + for Pj in projectors: + Pj_list[Pj.label] = Pj + + self.Save_label_Pj_dict(Pj_list, labels, path, Name) + + # Load projectors previously saved under a disk folder + @classmethod + def load_PoolMap_bulk_list(cls, path): + """check if the projector directory exists + + Args: + path (str): path to store the projectors + """ + + label_ALL_file = "{}/ALL_labels.pickle".format(path) + if os.path.exists(label_ALL_file): + print("{} exists".format(label_ALL_file)) + else: + print(" Projectors ALL in one file \n") + + @classmethod + def load_labels_from_file(cls, path): + f_label = "{}/labels.pickle".format(path) + with open(f_label, "rb") as f: + labels = pickle.load(f) + print(" ---> loading labels DONE from file = {}".format(f_label)) + + return labels + + # Load projectors previously saved under a disk folder + @classmethod + def load_PoolMap(cls, path, labels=None): + """to load the stored Pauli projectors + + Args: + path (str): the path to the projectors + labels (list, optional): list of Pauli operator labels. Defaults to None. + + Returns: + dict: (projector_dict) dictionary with values referring to the projectors + """ + + if labels == None: + labels = cls.load_labels_from_file(path) + + fname = "{}/Pj_list.pickle".format(path) + with open(fname, "rb") as f: + Pj_list = pickle.load(f) + + projector_dict = {} + for label in labels: + projector_dict[label] = Pj_list[label] + + return projector_dict + + # ------------------------------------------------- # + # below the original method saving each projector # + # ------------------------------------------------- # + # Generate and save the projectors and do it in parallel, . + # i.e. using all available cores in your system + def populate(self, path): + format = "hdf5" + if not path.endswith(".hdf5"): + format = "pickle" + if not os.path.exists(path): + os.mkdir(path) + + num_cpus = multiprocessing.cpu_count() + num_cpus2 = num_cpus - 2 + num_rounds = 1 + if self.size > num_cpus2: + num_rounds = self.size // num_cpus2 + 1 + + # XXX lock defies parallelization in generattion for hdf5 + # XXX Consider using MPI-based scheme + if format == "hdf5": + lock = multiprocessing.Lock() + + print( + " -- num_cpus = {}, num_cpus2= {}, size = {} -- ".format( + num_cpus, num_cpus2, self.size + ) + ) + print(" ******* num_rounds = {} *******".format(num_rounds)) + + for r in range(num_rounds): + process_list = [] + for t in range(num_cpus2): + idx = r * num_cpus2 + t + if idx == self.size: + break + label = self.labels[idx] + + print("r = {}, t = {}, idx = {}, label = {}".format(r, t, idx, label)) + + if format == "pickle": + process = multiprocessing.Process( + target=_pickle_saver, args=(label, path) + ) + elif format == "hdf5": + process = multiprocessing.Process( + target=_hdf5_saver, args=(label, path, lock) + ) + + process.start() + process_list.append(process) + + # moving join() inside the outer loop to avoid "too many files open" error + for p in process_list: + p.join() + + @classmethod + def load_labels(cls, path, Nk=None): + if path.endswith(".hdf5"): + with h5py.File(path, "r") as f: + labels = f.keys() + else: + try: + label_load1 = [fname.split(".")[0] for fname in os.listdir(path)] + + label_load2 = [ + label for label in label_load1 if not label.startswith("Pj_list") + ] + label_load3 = [ + label for label in label_load2 if not label.startswith("labels") + ] + labels = [ + label for label in label_load3 if not label.startswith("ALL_") + ] + + if Nk != None: + labels = [label for label in labels if len(label) == Nk] + + except: + fragment_paths = [ + os.path.join(path, fragment) for fragment in os.listdir(path) + ] + labels = [] + for fragment_path in fragment_paths: + fragment_labels = [ + fname.split(".")[0] for fname in os.listdir(fragment_path) + ] + labels.extend(fragment_labels) + return labels + + # Load projectors previously saved under a disk folder + @classmethod + def load(cls, path, labels=None): + """to load all the stored Pauli projectors from a given path + + Args: + path (str): the path to the projectors + labels (list, optional): list of Pauli operator labels. Defaults to None. + + Returns: + dict: (projector_dict) the dictionary with key of Pauli operator labels + and value of Pauli operators themselves + """ + + if labels == None: + labels = cls.load_labels(path) + + # checking if the store is fragmented and compute num_leading_symbols + names = os.listdir(path) + aname = names[0] + apath = os.path.join(path, aname) + if os.path.isdir(apath): # is a directory or not (not checking file) + num_leading_symbols = len(aname) + else: + num_leading_symbols = 0 + + # load the store + projectors = [ + Projector.load(path, label, num_leading_symbols) for label in labels + ] + projector_dict = {} + for label, projector in zip(*[labels, projectors]): + projector_dict[label] = projector + return projector_dict diff --git a/src/qibo/tomography_RGD/qibo_states.py b/src/qibo/tomography_RGD/qibo_states.py new file mode 100755 index 0000000000..9bab360c53 --- /dev/null +++ b/src/qibo/tomography_RGD/qibo_states.py @@ -0,0 +1,258 @@ +import random + +import numpy as np + +import qibo +from qibo import gates +from qibo.quantum_info import random_density_matrix + + +class State: + def __init__(self, n): + """ + Initializes State class + - n: number of qubits + - quantum register: object to hold quantum information + - classical register: object to hold classical information + - circuit_name: circuit name; defined in each subclass (GHZState, HadamardState, RandomState) + """ + + self.n = n + + self.circuit_name = None + self.circuit = None + self.measurement_circuit_names = [] + self.measurement_circuits = [] + + def create_circuit(self): + raise NotImplemented + + def execute_circuit(self): + # XXX not needed? + pass + + def get_state_vector(self): + """ + Executes circuit by connecting to Qiskit object, and obtain state vector + """ + if self.circuit is None: + self.create_circuit() + + # XXX add probe? + qibo.set_backend("numpy") + result = self.circuit.execute() + state_vector = result.to_dict()["state"] + return state_vector + + def get_state_matrix(self): + """ + Obtain density matrix by taking an outer product of state vector + """ + state_vector = self.get_state_vector() + state_matrix = np.outer(state_vector, state_vector.conj()) + return state_matrix + + def create_measurement_circuits(self, labels, label_format="little_endian"): + """ + Prepares measurement circuits + - labels: string of Pauli matrices (e.g. XYZXX) + """ + + if self.circuit is None: + self.create_circuit() + + qubits = range(self.n) + + for label in labels: + + # for aligning to the natural little_endian way of iterating through bits below + if label_format == "big_endian": + effective_label = label[::-1] + else: + effective_label = label[:] + probe_circuit = qibo.Circuit(self.n) + + for qubit, letter in zip(*[qubits, effective_label]): + if letter == "X": + probe_circuit.add(gates.H(qubit)) # H + + elif letter == "Y": + probe_circuit.add(gates.S(qubit).dagger()) # S^dg + probe_circuit.add(gates.H(qubit)) # H + + probe_circuit.add(gates.M(*qubits)) + + measurement_circuit_name = self.make_measurement_circuit_name( + self.circuit_name, label + ) + measurement_circuit = self.circuit + probe_circuit + self.measurement_circuit_names.append(measurement_circuit_name) + self.measurement_circuits.append(measurement_circuit) + + @staticmethod + def make_measurement_circuit_name(circuit_name, label): + """ + Measurement circuit naming convention + """ + name = "{}-{}".format(circuit_name, label) + return name + + @staticmethod + def sort_sample(sample) -> dict: + """ + Sort raw measurements into count dictionary + """ + sorted_sample = {} + for shot in sample: + s = "".join(map(str, shot)) + if s in sorted_sample: + sorted_sample[s] += 1 + else: + sorted_sample[s] = 1 + return sorted_sample + + def execute_measurement_circuits( + self, + labels, + backend="numpy", + num_shots=100, + # num_shots = 10000, + label_format="little_endian", + ): + """ + Executes measurement circuits + - labels: string of Pauli matrices (e.g. XYZXX) + - backend: 'numpy', 'qibojit', 'pytorch', 'tensorflow' prvided by qibo + - num_shots: number of shots measurement is taken to get empirical frequency through counts + """ + if self.measurement_circuit_names == []: + self.create_measurement_circuits(labels, label_format) + + circuit_names = self.measurement_circuit_names + + qibo.set_backend(backend) + + data_dict_list = [] + for i, label in enumerate(labels): + result = self.measurement_circuits[i].execute(nshots=num_shots) + result.samples(binary=True, registers=False) + count_dict = self.sort_sample(result.to_dict()["samples"]) + + measurement_circuit_name = self.make_measurement_circuit_name( + self.circuit_name, label + ) + data_dict = { + "measurement_circuit_name": measurement_circuit_name, + "circuit_name": self.circuit_name, + "label": label, + "count_dict": count_dict, + "backend": "qibo: " + backend, + "num_shots": num_shots, + } + data_dict_list.append(data_dict) + return data_dict_list + + +class GHZState(State): + """ + Constructor for GHZState class + """ + + def __init__(self, n): + State.__init__(self, n) + self.circuit_name = "GHZ" + + def create_circuit(self): + circuit = qibo.Circuit(self.n) + + circuit.add(gates.H(0)) + for i in range(1, self.n): + circuit.add(gates.CNOT(0, i)) + + self.circuit = circuit + + +class HadamardState(State): + """ + Constructor for HadamardState class + """ + + def __init__(self, n): + State.__init__(self, n) + self.circuit_name = "Hadamard" + + def create_circuit(self): + circuit = qibo.Circuit(self.n) + + for i in range(self.n): + circuit.add(gates.H(i)) + + self.circuit = circuit + + +class RandomState(State): + """ + Constructor for RandomState class + """ + + def __init__(self, n, seed=0, depth=40): + State.__init__(self, n) + self.circuit_name = "Random-%d" % (self.n,) + + self.seed = seed + self.depth = depth + + def create_circuit(self): + random.seed(a=self.seed) + circuit = qibo.Circuit(self.n) + + for j in range(self.depth): + if self.n == 1: + op_ind = 0 + else: + op_ind = random.randint(0, 1) + if op_ind == 0: # U3 + qind = random.randint(0, self.n - 1) + circuit.add( + gates.U3( + qind, + 2 * np.pi * random.random(), + 2 * np.pi * random.random(), + 2 * np.pi * random.random(), + trainable=True, + ) + ) + elif op_ind == 1: # CX + source, target = random.sample(range(self.n), 2) + circuit.add(gates.CNOT(source, target)) + + self.circuit = circuit + + +if __name__ == "__main__": + + ############################################################ + ### Example of creating and running an experiment + ############################################################ + + n = 3 + # labels = projectors.generate_random_label_list(20, n) + labels = ["YXY", "IXX", "ZYI", "XXX", "YZZ"] + # labels = ['YZYX', 'ZZIX', 'XXIZ', 'XZIY', 'YXYI', 'ZYYX', 'YXXX', 'IIYY', 'ZIXZ', 'IXXI', 'YZXI', 'ZZYI', 'YZXY', 'XYZI', 'XZXI', 'XZYX', 'YIXI', 'IZYY', 'ZIZX', 'YXXY'] + # labels = ['IIIX', 'IYIY', 'YYXI', 'ZZYY', 'ZYIX', 'XIII', 'XXZI', 'YXZI', 'IZXX', 'YYIZ', 'XXIY', 'XXZY', 'ZZIY', 'YIYX', 'YYZZ', 'YZXZ', 'YZYZ', 'ZXYY', 'IXIZ', 'XZII'] + # labels = Generate_All_labels(n) + + # state = GHZState(n) + # state = HadamardState(n) + state = RandomState(n) + + state.create_circuit() + data_dict_list = state.execute_measurement_circuits(labels) + # print(data_dict_list) + + target_density_matrix = state.get_state_matrix() + target_state = state.get_state_vector() + # print(state.get_state_vector()) + + Nr = 3 + random_DM = random_density_matrix(2**n, Nr)