diff --git a/src/qutip_qip/decompose/_utility.py b/src/qutip_qip/decompose/_utility.py index 99ca8f90..cef4791f 100644 --- a/src/qutip_qip/decompose/_utility.py +++ b/src/qutip_qip/decompose/_utility.py @@ -29,3 +29,64 @@ def check_gate(gate, num_qubits): raise ValueError("Input is not unitary.") if gate.dims != [[2] * num_qubits] * 2: raise ValueError(f"Input is not a unitary on {num_qubits} qubits.") + + +def _binary_sequence(num_qubits): + """ Defines the binary sequence list for basis vectors of a n-qubit gate. + The string at index `i` is also the row/column index for a basis vector in + a numpy array. + """ + old_sequence = ['0', '1'] + full_binary_sequence = [] + + if num_qubits == 1: + full_binary_sequence = old_sequence + else: + for x in range(num_qubits-1): + full_binary_sequence = [] + zero_append_sequence = ['0' + x for x in old_sequence] + full_binary_sequence.extend(zero_append_sequence) + one_append_sequence = ['1' + x for x in old_sequence] + full_binary_sequence.extend(one_append_sequence) + old_sequence = full_binary_sequence + + return(full_binary_sequence) + + +def _gray_code_sequence(num_qubits, output_form=None): + """ Finds the sequence of gray codes for basis vectors by using logical + XOR operator of Python. + For print( _gray_code_sequence(2)), the output is [0, 1, 3, 2] i.e. in + terms of the binary sequence, the output is ['00', '01', '11', '10']. + https://docs.python.org/3/library/operator.html#operator.xor' + + Parameters + ---------- + num_qubits + Number of qubits in the circuit + output_form : :"index_values" or None + The format of output list. If a string "index_values" is provided then + the function's output is in terms of array indices of the binary sequence. + The default is a list of binary strings. + + Returns + -------- + list + List of the gray code sequence in terms of array indices or binary + sequence positions. + """ + gray_code_sequence_as_array_indices = [] + + for x in range(2**num_qubits): + gray_code_at_x = x ^ x // 2 # floor operator to shift bits by 1 + # when the shift is done, the new spot is filled with a new value. + gray_code_sequence_as_array_indices.append(gray_code_at_x) + if output_form == "index_values": + output = gray_code_sequence_as_array_indices + else: + gray_code_as_binary = [] + binary_sequence_list = _binary_sequence(num_qubits) + for i in gray_code_sequence_as_array_indices: + gray_code_as_binary.append(binary_sequence_list[i]) + output = gray_code_as_binary + return(output) diff --git a/src/qutip_qip/decompose/decompose_general_qubit_gate.py b/src/qutip_qip/decompose/decompose_general_qubit_gate.py new file mode 100644 index 00000000..21edb681 --- /dev/null +++ b/src/qutip_qip/decompose/decompose_general_qubit_gate.py @@ -0,0 +1,178 @@ +import numpy as np +import cmath +from qutip import Qobj +from qutip_qip.decompose._utility import ( + check_gate, + _binary_sequence, + _gray_code_sequence, +) + + +def _decompose_to_two_level_arrays(input_gate, num_qubits, expand=True): + """Decompose a general qubit gate to two-level arrays. + + Parameters + ----------- + input_gate : :class:`qutip.Qobj` + The gate matrix to be decomposed. + num_qubits : int + Number of qubits being acted upon by the input_gate + expand : True + Default parameter to return the output as full two-level Qobj. If + `expand = False` then the function returns a tuple of index information + and a 2 x 2 Qobj for each gate. The Qobj are returned in reversed order. + """ + check_gate(input_gate, num_qubits) + input_array = input_gate.full() + + # Calculate the two level numpy arrays + array_list = [] + index_list = [] + for i in range(2 ** num_qubits): + for j in range(i + 1, 2 ** num_qubits): + new_index = [i, j] + index_list.append(new_index) + + for i in range(len(index_list) - 1): + index_1, index_2 = index_list[i] + + # Values of single qubit U forming the two level unitary + a = input_array[index_1][index_1] + a_star = np.conj(a) + b = input_array[index_2][index_1] + b_star = np.conj(b) + norm_constant = cmath.sqrt( + np.absolute(a * a_star) + np.absolute(b * b_star) + ) + + # Create identity array and then replace with above values for + # index_1 and index_2 + U_two_level = np.identity(2 ** num_qubits, dtype=complex) + U_two_level[index_1][index_1] = a_star / norm_constant + U_two_level[index_2][index_1] = b / norm_constant + U_two_level[index_1][index_2] = b_star / norm_constant + U_two_level[index_2][index_2] = -a / norm_constant + + # Change input by multiplying by above two-level + input_array = np.dot(U_two_level, input_array) + + # U dagger to calculate the gates + U__two_level_dagger = np.transpose(np.conjugate(U_two_level)) + array_list.append(U__two_level_dagger) + + # for U6 - multiply input array by U5 and take dagger + U_last_dagger = input_array + array_list.append(U_last_dagger) + + if expand is True: + array_list_with_qobj = [] + for i in reversed(range(len(index_list))): + U_two_level_array = array_list[i] + array_list_with_qobj.append( + Qobj(U_two_level_array, dims=[[2] * num_qubits] * 2) + ) + return array_list_with_qobj + else: + compact_U_information = [] + for i in reversed(range(len(index_list))): + U_non_trivial = np.full([2, 2], None, dtype=complex) + index_info = [] + U_index_together = [] + + # create index list + index_1, index_2 = index_list[i] + index_info = [index_1, index_2] + U_index_together.append(index_info) + + # create 2 x 2 arrays + U_two_level = array_list[i] + U_non_trivial[0][0] = U_two_level[index_1][index_1] + U_non_trivial[1][0] = U_two_level[index_2][index_1] + U_non_trivial[0][1] = U_two_level[index_1][index_2] + U_non_trivial[1][1] = U_two_level[index_2][index_2] + U_index_together.append(Qobj(U_non_trivial, dims=[[2] * 1] * 2)) + + compact_U_information.append(U_index_together) + + return compact_U_information + + +def _create_dict_for_two_level_arrays(two_level_output): + """Creates a dictionary with keys for the total number of two-level array + output. This will be used by other functions to store information about + SWAP, PauliX gates etc. + """ + num_two_level_gates = len(two_level_output) + + # create a reversed list of keys based on total number of two-level gates + # ranging from 1 to n where n is the total number of two-level arrays + gate_keys = list(range(1, num_two_level_gates + 1))[::-1] + gate_info_dict = dict.fromkeys(gate_keys) + return gate_info_dict + + +def _partial_gray_code(num_qubits, two_level_output): + """Returns a dictionary of partial gray code sequence for each two-level + array. + + The input is when output from decomposition array output is non-expanded.""" + + # create empty dict + gate_key_dict = _create_dict_for_two_level_arrays(two_level_output) + + # create a list of non-trivial indices in two level array output + two_level_indices = [] + for i in range(len(two_level_output)): + two_level_indices.append(two_level_output[i][0]) + + # gray code sequence output as indices of binary sequence and strings + # respectively + gray_code_index = _gray_code_sequence(num_qubits, "index_values") + gray_code_string = _gray_code_sequence(num_qubits) + + # get the partial gray code sequence + for i in range(len(two_level_indices)): + partial_gray_code = [] + ind1 = two_level_indices[i][0] + ind2 = two_level_indices[i][1] + + ind1_pos_in_gray_code = gray_code_index.index(ind1) + ind2_pos_in_gray_code = gray_code_index.index(ind2) + + if ind1_pos_in_gray_code > ind2_pos_in_gray_code: + partial_gray_code = [ind2_pos_in_gray_code, ind1_pos_in_gray_code] + else: + partial_gray_code = [ind1_pos_in_gray_code, ind2_pos_in_gray_code] + + gate_key_dict[len(two_level_indices) - i] = gray_code_string[ + partial_gray_code[0] : partial_gray_code[1] + 1 + ] + + return gate_key_dict + + +def _split_partial_gray_code(gate_key_dict): + """Splits the output of gray code sequence into n-bit Toffoli and + two-level array gate of interest. + + The output is a list of dictionary of n-bit toffoli and another dictionary + for the gate needing to be decomposed. + + When the decomposed gates are added to the circuit, n-bit toffoli will be + used twice - once in the correct order it is and then in a reversed order. + + For cases where there is only 1 step in the gray code sequence, first dictionary + will be empty and second will need a decomposition scheme. + """ + n_bit_toffoli_dict = {} + two_level_of_int = {} + for key in gate_key_dict.keys(): + if len(gate_key_dict[key]) > 2: + key_value = gate_key_dict[key] + two_level_of_int[key] = [key_value[-1], key_value[-2]] + n_bit_toffoli_dict[key] = key_value[0:-2] + else: + two_level_of_int[key] = gate_key_dict[key] + n_bit_toffoli_dict[key] = None + output_of_separated_gray_code = [n_bit_toffoli_dict, two_level_of_int] + return output_of_separated_gray_code diff --git a/tests/decomposition_functions/test_general_decomposition.py b/tests/decomposition_functions/test_general_decomposition.py new file mode 100644 index 00000000..1f0fcdd3 --- /dev/null +++ b/tests/decomposition_functions/test_general_decomposition.py @@ -0,0 +1,268 @@ +import numpy as np +import pytest + +from qutip import Qobj, average_gate_fidelity, rand_unitary + + +from qutip_qip.decompose.decompose_general_qubit_gate import ( + _decompose_to_two_level_arrays, + _create_dict_for_two_level_arrays, + _partial_gray_code, + _split_partial_gray_code, +) + + +@pytest.mark.parametrize("num_qubits", [2, 3, 4, 5, 6]) +def test_two_level_full_output(num_qubits): + """Check if product of full two level array output is equal to the input.""" + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=True + ) + + product_of_U = array_decompose[-1] + + for i in reversed(range(len(array_decompose) - 1)): + product_of_U = product_of_U + product_of_U_calculated = np.dot(product_of_U, array_decompose[i]) + product_of_U = product_of_U_calculated + + product_of_U = Qobj(product_of_U, dims=[[2] * num_qubits] * 2) + fidelity_of_input_output = average_gate_fidelity(product_of_U, input_gate) + assert np.isclose(fidelity_of_input_output, 1.0) + + +@pytest.mark.parametrize("num_qubits", [2, 3, 4, 5]) +def test_empty_dict_of_two_level_arrays(num_qubits): + """Check if empty dictionary is of the same length as + the two-level array output. + """ + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + empty_dict_output = _create_dict_for_two_level_arrays(array_decompose) + assert np.equal(len(empty_dict_output), len(array_decompose)) + + +@pytest.mark.parametrize("num_qubits", [2, 3, 4, 5]) +def test_len_partial_grey_code(num_qubits): + """Check if split gray code output is of the same length + as the two-level array output. + """ + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + empty_dict_output = _partial_gray_code(num_qubits, array_decompose) + assert np.equal(len(empty_dict_output), len(array_decompose)) + + +@pytest.mark.parametrize("num_qubits", [2, 3, 4, 5]) +def test_keys_partial_grey_code(num_qubits): + """Check if dictionary keys are consistent in partial grey code. + + The keys are for all two-level gates describing the decomposition and + are in a reversed order. + """ + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + dict_output = _partial_gray_code(num_qubits, array_decompose) + gate_key_list = list( + _partial_gray_code(num_qubits, array_decompose).keys() + ) + correct_gate_key_list = list(range(1, len(array_decompose) + 1)[::-1]) + assert gate_key_list == correct_gate_key_list + + +def test_two_qubit_partial_grey_code_output(): + """Checks if the gray code output is as expected.""" + num_qubits = 2 + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + func_output = _partial_gray_code(num_qubits, array_decompose) + expected_output = { + 6: ["11", "10"], + 5: ["01", "11"], + 4: ["01", "11", "10"], + 3: ["00", "01", "11"], + 2: ["00", "01", "11", "10"], + 1: ["00", "01"], + } + assert func_output == expected_output + + +def test_three_qubit_partial_grey_code_output(): + """Checks if the gray code output is as expected.""" + num_qubits = 3 + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + func_output = _partial_gray_code(num_qubits, array_decompose) + expected_output = { + 28: ["110", "111"], + 27: ["111", "101"], + 26: ["110", "111", "101"], + 25: ["111", "101", "100"], + 24: ["110", "111", "101", "100"], + 23: ["101", "100"], + 22: ["011", "010", "110", "111"], + 21: ["011", "010", "110"], + 20: ["011", "010", "110", "111", "101"], + 19: ["011", "010", "110", "111", "101", "100"], + 18: ["010", "110", "111"], + 17: ["010", "110"], + 16: ["010", "110", "111", "101"], + 15: ["010", "110", "111", "101", "100"], + 14: ["011", "010"], + 13: ["001", "011", "010", "110", "111"], + 12: ["001", "011", "010", "110"], + 11: ["001", "011", "010", "110", "111", "101"], + 10: ["001", "011", "010", "110", "111", "101", "100"], + 9: ["001", "011"], + 8: ["001", "011", "010"], + 7: ["000", "001", "011", "010", "110", "111"], + 6: ["000", "001", "011", "010", "110"], + 5: ["000", "001", "011", "010", "110", "111", "101"], + 4: ["000", "001", "011", "010", "110", "111", "101", "100"], + 3: ["000", "001", "011"], + 2: ["000", "001", "011", "010"], + 1: ["000", "001"], + } + assert func_output == expected_output + + +@pytest.mark.parametrize("num_qubits", [2, 3, 4, 5]) +def test_len_split_grey_code(num_qubits): + """Check if length of both dict outputs of split gray code is equal to + the total number of two-level array gates. + + First dict is made up of n-bit toffoli and second is made up of + gate that needs decomposition. + """ + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + len_two_level_array = len(array_decompose) + func_out = _split_partial_gray_code( + _partial_gray_code(num_qubits, array_decompose) + ) + + # Checks if 2 separate dictionaries are always returned + assert len(func_out) == 2 + + # check length of each dictionary + assert len(func_out[0]) == len(array_decompose) + assert len(func_out[1]) == len(array_decompose) + + +def test_two_qubit_split_partial_grey_code(): + """Checks if output of split partial gray code function is correct + with expected output for two qubits.""" + num_qubits = 2 + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + split_output = _split_partial_gray_code( + _partial_gray_code(num_qubits, array_decompose) + ) + expected_toffoli_output = { + 6: None, + 5: None, + 4: ["01"], + 3: ["00"], + 2: ["00", "01"], + 1: None, + } + expected_gate_decom_out = { + 6: ["11", "10"], + 5: ["01", "11"], + 4: ["10", "11"], + 3: ["11", "01"], + 2: ["10", "11"], + 1: ["00", "01"], + } + assert expected_toffoli_output == split_output[0] + assert expected_gate_decom_out == split_output[1] + + +def test_three_qubit_split_partial_grey_code(): + """Checks if output of split partial gray code function is correct + with expected output for three qubits.""" + num_qubits = 3 + input_gate = rand_unitary(2 ** num_qubits, dims=[[2] * num_qubits] * 2) + array_decompose = _decompose_to_two_level_arrays( + input_gate, num_qubits, expand=False + ) + split_output = _split_partial_gray_code( + _partial_gray_code(num_qubits, array_decompose) + ) + expected_toffoli_output = { + 28: None, + 27: None, + 26: ["110"], + 25: ["111"], + 24: ["110", "111"], + 23: None, + 22: ["011", "010"], + 21: ["011"], + 20: ["011", "010", "110"], + 19: ["011", "010", "110", "111"], + 18: ["010"], + 17: None, + 16: ["010", "110"], + 15: ["010", "110", "111"], + 14: None, + 13: ["001", "011", "010"], + 12: ["001", "011"], + 11: ["001", "011", "010", "110"], + 10: ["001", "011", "010", "110", "111"], + 9: None, + 8: ["001"], + 7: ["000", "001", "011", "010"], + 6: ["000", "001", "011"], + 5: ["000", "001", "011", "010", "110"], + 4: ["000", "001", "011", "010", "110", "111"], + 3: ["000"], + 2: ["000", "001"], + 1: None, + } + expected_gate_decom_out = { + 28: ["110", "111"], + 27: ["111", "101"], + 26: ["101", "111"], + 25: ["100", "101"], + 24: ["100", "101"], + 23: ["101", "100"], + 22: ["111", "110"], + 21: ["110", "010"], + 20: ["101", "111"], + 19: ["100", "101"], + 18: ["111", "110"], + 17: ["010", "110"], + 16: ["101", "111"], + 15: ["100", "101"], + 14: ["011", "010"], + 13: ["111", "110"], + 12: ["110", "010"], + 11: ["101", "111"], + 10: ["100", "101"], + 9: ["001", "011"], + 8: ["010", "011"], + 7: ["111", "110"], + 6: ["110", "010"], + 5: ["101", "111"], + 4: ["100", "101"], + 3: ["011", "001"], + 2: ["010", "011"], + 1: ["000", "001"], + } + assert expected_toffoli_output == split_output[0] + assert expected_gate_decom_out == split_output[1]