From 0dd97033166a547bcb2608d5a17ecc28d3cda5f3 Mon Sep 17 00:00:00 2001 From: JoshuaLau0220 Date: Thu, 29 Feb 2024 15:31:29 +0800 Subject: [PATCH] :recycle: remove the need to inject num qubit for tensor decomposer --- src/convert/conversion_cmd.cpp | 5 +- src/tensor/decomposer.cpp | 48 ++ src/tensor/decomposer.hpp | 911 +++++++++++++++++---------------- 3 files changed, 505 insertions(+), 459 deletions(-) create mode 100644 src/tensor/decomposer.cpp diff --git a/src/convert/conversion_cmd.cpp b/src/convert/conversion_cmd.cpp index e01eb7e4..f8b6c65e 100644 --- a/src/convert/conversion_cmd.cpp +++ b/src/convert/conversion_cmd.cpp @@ -163,10 +163,7 @@ Command conversion_cmd(QCirMgr& qcir_mgr, qsyn::tensor::TensorMgr& tensor_mgr, q if (!dvlab::utils::mgr_has_data(tensor_mgr)) return CmdExecResult::error; spdlog::info("Converting Tensor {} to QCir {}...", tensor_mgr.focused_id(), qcir_mgr.get_next_id()); - auto ts = tensor_mgr.get(); - const size_t qreg = size_t(log2((*ts).shape()[0])); - tensor::Decomposer decomp(qreg); - auto result = decomp.decompose(*ts); + auto const result = tensor::Decomposer{}.decompose(*tensor_mgr.get()); if (result) { qcir_mgr.add(qcir_mgr.get_next_id(), std::make_unique(std::move(*result))); diff --git a/src/tensor/decomposer.cpp b/src/tensor/decomposer.cpp new file mode 100644 index 00000000..a7960f81 --- /dev/null +++ b/src/tensor/decomposer.cpp @@ -0,0 +1,48 @@ +/**************************************************************************** + PackageName [ tensor ] + Synopsis [ Define tensor decomposer structure ] + Author [ Design Verification Lab ] + Copyright [ Copyright(c) 2023 DVLab, GIEE, NTU, Taiwan ] +****************************************************************************/ + +#include "./decomposer.hpp" + +namespace qsyn::tensor { + +/** + * @brief Append gate and save the encoding gate sequence + * + * @param target + * @param qubit_list + * @param gate_list + */ +void Decomposer::encode_control_gate(QubitIdList const& target, std::vector& qubit_list, std::vector& gate_list) { + qubit_list.emplace_back(target); + gate_list.emplace_back(target.size() == 2 ? "cx" : "x"); + _quantum_circuit.add_gate(target.size() == 2 ? "cx" : "x", target, {}, true); +} + +/** + * @brief Gray encoder + * + * @param origin_pos Origin qubit + * @param targ_pos Target qubit + * @param qubit_list + * @param gate_list + */ +void Decomposer::encode(size_t origin_pos, size_t targ_pos, std::vector& qubit_list, std::vector& gate_list) { + bool x_given = 0; + if (((origin_pos >> targ_pos) & 1) == 0) { + encode_control_gate({int(targ_pos)}, qubit_list, gate_list); + x_given = 1; + } + for (size_t i = 0; i < _n_qubits; i++) { + if (i == targ_pos) continue; + if (((origin_pos >> i) & 1) == 0) + encode_control_gate({int(targ_pos), int(i)}, qubit_list, gate_list); + } + if (x_given) + encode_control_gate({int(targ_pos)}, qubit_list, gate_list); +} + +} // namespace qsyn::tensor diff --git a/src/tensor/decomposer.hpp b/src/tensor/decomposer.hpp index b08c3c51..c2822e3d 100644 --- a/src/tensor/decomposer.hpp +++ b/src/tensor/decomposer.hpp @@ -1,6 +1,6 @@ /**************************************************************************** - PackageName [ extractor ] - Synopsis [ Define class Extractor structure ] + PackageName [ tensor ] + Synopsis [ Define tensor decomposer structure ] Author [ Design Verification Lab ] Copyright [ Copyright(c) 2023 DVLab, GIEE, NTU, Taiwan ] ****************************************************************************/ @@ -11,6 +11,7 @@ #include #include +#include #include "qcir/qcir.hpp" #include "qsyn/qsyn_type.hpp" @@ -28,13 +29,18 @@ using tensor::Tensor; namespace tensor { template - struct TwoLevelMatrix { - TwoLevelMatrix(QTensor const& m, size_t i, size_t j) : _matrix(m), _i(i), _j(j) {} + TwoLevelMatrix(QTensor const& m, size_t i, size_t j) : _matrix(m), _i(i), _j(j) { assert(i < j); } QTensor _matrix; size_t _i = 0, _j = 0; // i < j }; +template +TwoLevelMatrix adjoint(TwoLevelMatrix m /* copy on purpose */) { + m._matrix.adjoint(); + return m; +} + struct ZYZ { double phi; double alpha; @@ -46,531 +52,526 @@ struct ZYZ { class Decomposer { private: QCir _quantum_circuit; - size_t _qreg; + size_t _n_qubits = 0; public: template - friend struct TwoLevelMatrix; - friend struct ZYZ; + std::optional decompose(QTensor const& matrix); - Decomposer(size_t reg) : _quantum_circuit{reg}, _qreg(reg) {} - - size_t get_qreg() { return _qreg; } +private: + template + TwoLevelMatrix make_two_level_matrix(QTensor const& matrix, size_t i, size_t j) { + auto const kernel = + QTensor({{matrix(i, i), matrix(i, j)}, + {matrix(j, i), matrix(j, j)}}); + return TwoLevelMatrix(kernel, i, j); + } /** - * @brief Convert the matrix into a quantum circuit + * @brief assumes that the input matrix is a square matrix * * @tparam U * @param matrix - * @return QCir* + * @return size_t */ template - std::optional decompose(QTensor const& matrix) { - auto mat_chain = get_two_level_matrices(matrix); - - for (auto const& i : std::views::iota(0UL, mat_chain.size()) | std::views::reverse) { - size_t i_idx = 0, j_idx = 0; - for (size_t j = 0; j < _qreg; j++) { - i_idx = i_idx * 2 + (mat_chain[i]._i >> j & 1); - j_idx = j_idx * 2 + (mat_chain[i]._j >> j & 1); - } - if (i_idx > j_idx) { - std::swap(i_idx, j_idx); - std::swap(mat_chain[i]._matrix(0, 0), mat_chain[i]._matrix(1, 1)); - std::swap(mat_chain[i]._matrix(0, 1), mat_chain[i]._matrix(1, 0)); - } - - if (!graycode(mat_chain[i]._matrix, i_idx, j_idx)) return std::nullopt; - } - return _quantum_circuit; + size_t get_dimension(Tensor const& matrix) { + DVLAB_ASSERT(matrix.shape().size() == 2 && matrix.shape()[0] == matrix.shape()[1], "Matrix is not square"); + return static_cast(matrix.shape()[0]); } - /** - * @brief If the input matrix is a two-level matrix, return the indices of the two-level matrix - * - * @tparam U - * @param matrix - * @param eps - * @return std::optional> - */ template - std::optional> get_two_level_matrix_indices(QTensor const& matrix, double eps) { - using namespace std::literals; - auto const dimension = static_cast(matrix.shape()[0]); - size_t num_found_diagonal = 0, num_upper_triangle_not_zero = 0, num_lower_triangle_not_zero = 0; - size_t top_main_diagonal_coords = 0, top_sub_diagonal_row = 0, bottom_sub_diagonal_row = 0; - size_t bottom_main_diagonal_coords = 0, top_sub_diagonal_col = 0, bottom_sub_diagonal_col = 0; - - // count the number of non-1 elements in the diagonal - // and the non-zero elements in the upper and lower triangles - for (size_t x = 0; x < dimension; x++) { - for (size_t y = 0; y < dimension; y++) { - if (x == y) { - if (std::abs(matrix(y, x) - (1.0 + 0.i)) > eps) { - num_found_diagonal++; - if (num_found_diagonal == 1) - top_main_diagonal_coords = x; - if (num_found_diagonal == 2) - bottom_main_diagonal_coords = x; - } - } else if (x > y) { // Upper diagonal - if (std::abs(matrix(y, x)) > eps) { - num_upper_triangle_not_zero++; - top_sub_diagonal_row = y; - top_sub_diagonal_col = x; - } - } else { // Lower diagonal - if (std::abs(matrix(y, x)) > eps) { - num_lower_triangle_not_zero++; - bottom_sub_diagonal_row = y; - bottom_sub_diagonal_col = x; - } - } - } - } + std::optional> get_two_level_matrix_indices(QTensor const& matrix, double eps); + template + std::vector> get_two_level_matrices(QTensor matrix /* copy on purpose */); - // is the matrix a two-level one? + template + bool graycode(Tensor const& matrix, size_t i, size_t j); + void encode(size_t origin_pos, size_t targ_pos, std::vector& qubit_list, std::vector& gate_list); + void encode_control_gate(QubitIdList const& target, std::vector& qubit_list, std::vector& gate_list); - // case 1: some off-diagonal elements are non-zero - if ((num_upper_triangle_not_zero == 1 && num_lower_triangle_not_zero == 1) && (top_sub_diagonal_row == bottom_sub_diagonal_col && top_sub_diagonal_col == bottom_sub_diagonal_row)) { - // kernel is [x x] - // [x x] - if (num_found_diagonal == 2 && top_main_diagonal_coords == top_sub_diagonal_row && bottom_main_diagonal_coords == bottom_sub_diagonal_row) { - return std::make_pair(top_main_diagonal_coords, bottom_main_diagonal_coords); - } - // REVIEW - why is this condition necessary? - if (num_found_diagonal == 1 && (top_main_diagonal_coords == top_sub_diagonal_row || top_main_diagonal_coords == top_sub_diagonal_col)) { - if (top_main_diagonal_coords != dimension - 1) { - return std::make_pair(top_main_diagonal_coords, top_main_diagonal_coords + 1); - } else { - return std::make_pair(top_main_diagonal_coords - 1, top_main_diagonal_coords); - } - } - // kernel is [0 x] - // [x 0] - if (num_found_diagonal == 0) { - return std::make_pair(top_sub_diagonal_row, top_sub_diagonal_col); - } - } + template + bool decompose_CnU(Tensor const& t, size_t diff_pos, size_t index, size_t ctrl_gates); - // case 2: all off-diagonal elements are zero - if (num_upper_triangle_not_zero == 0 && num_lower_triangle_not_zero == 0) { - if (num_found_diagonal == 2) { - return std::make_pair(top_main_diagonal_coords, bottom_main_diagonal_coords); - } - // REVIEW - why is this condition necessary? - if (num_found_diagonal == 1) { - if (top_main_diagonal_coords != dimension - 1) { - return std::make_pair(top_main_diagonal_coords, top_main_diagonal_coords + 1); - } else { - return std::make_pair(top_main_diagonal_coords - 1, top_main_diagonal_coords); - } - } - return std::make_pair(SIZE_MAX, SIZE_MAX); // signals identity - // if (num_found_diagonal == 0) // identity - // return two_level_chain; - } + template + bool decompose_CnX(const std::vector& ctrls, const size_t extract_qubit, const size_t index, const size_t ctrl_gates); - return std::nullopt; - } + template + bool decompose_CU(Tensor const& t, size_t ctrl, size_t targ); - /** - * @brief Get the two-level matrices associated with the input matrix - * - * @tparam U - * @param matrix - * @return std::vector> List of two-level matrix - * @reference Li, Chi-Kwong, Rebecca Roberts, and Xiaoyan Yin. "Decomposition of unitary matrices and quantum gates." International Journal of Quantum Information 11.01 (2013): 1350015. - */ template - std::vector> get_two_level_matrices(QTensor matrix /* copy on purpose */) { - using namespace std::literals; - constexpr double eps = 1e-6; - std::vector> two_level_chain; - auto const dimension = static_cast(matrix.shape()[0]); - auto conjugate_matrix_product = - QTensor::identity(static_cast(std::round(std::log2(dimension)))); - conjugate_matrix_product.reshape({matrix.shape()[0], matrix.shape()[0]}); - - for (size_t i = 0; i < dimension; i++) { - for (size_t j = i + 1; j < dimension; j++) { - // if `matrix` is the last two-level matrix - if (auto const pair = get_two_level_matrix_indices(matrix, eps)) { - auto const& [selected_top, selected_bottom] = *pair; - - // shortcut for identity - if (selected_top == SIZE_MAX && selected_bottom == SIZE_MAX) { - return two_level_chain; - } - auto const two_level_kernel = QTensor{ - {matrix(selected_top, selected_top), matrix(selected_top, selected_bottom)}, - {matrix(selected_bottom, selected_top), matrix(selected_bottom, selected_bottom)}}; - - two_level_chain.emplace_back(TwoLevelMatrix(two_level_kernel, selected_top, selected_bottom)); - return two_level_chain; - } + ZYZ decompose_ZYZ(Tensor const& matrix); - // not a two-level matrix + template + Tensor sqrt_single_qubit_matrix(Tensor const& matrix); +}; + +/** + * @brief Convert the matrix into a quantum circuit + * + * @tparam U + * @param matrix + * @return QCir* + */ +template +std::optional Decomposer::decompose(QTensor const& matrix) { + _n_qubits = static_cast(std::round(std::log2(get_dimension(matrix)))); + auto mat_chain = get_two_level_matrices(matrix); + + _quantum_circuit = QCir(_n_qubits); + + for (auto const& i : std::views::iota(0UL, mat_chain.size()) | std::views::reverse) { + size_t i_idx = 0, j_idx = 0; + for (size_t j = 0; j < _n_qubits; j++) { + i_idx = i_idx * 2 + (mat_chain[i]._i >> j & 1); + j_idx = j_idx * 2 + (mat_chain[i]._j >> j & 1); + } + if (i_idx > j_idx) { + std::swap(i_idx, j_idx); + std::swap(mat_chain[i]._matrix(0, 0), mat_chain[i]._matrix(1, 1)); + std::swap(mat_chain[i]._matrix(0, 1), mat_chain[i]._matrix(1, 0)); + } - // if encounter additional zeros in the off-diagonal elements, skip - if (std::abs(matrix(i, i).real() - 1) < eps && std::abs(matrix(i, i).imag()) < eps) { // maybe use e-6 approx. - if (std::abs(matrix(j, i).real()) < eps && std::abs(matrix(j, i).imag()) < eps) { - continue; - } + if (!graycode(mat_chain[i]._matrix, i_idx, j_idx)) return std::nullopt; + } + return _quantum_circuit; +} + +/** + * @brief If the input matrix is a two-level matrix, return the indices of the two-level matrix + * + * @tparam U + * @param matrix + * @param eps + * @return std::optional> + */ +template +std::optional> Decomposer::get_two_level_matrix_indices(QTensor const& matrix, double eps) { + using namespace std::literals; + auto const dimension = static_cast(matrix.shape()[0]); + size_t num_found_diagonal = 0, num_upper_triangle_not_zero = 0, num_lower_triangle_not_zero = 0; + size_t top_main_diagonal_coords = 0, top_sub_diagonal_row = 0, bottom_sub_diagonal_row = 0; + size_t bottom_main_diagonal_coords = 0, top_sub_diagonal_col = 0, bottom_sub_diagonal_col = 0; + + // count the number of non-1 elements in the diagonal + // and the non-zero elements in the upper and lower triangles + for (size_t x = 0; x < dimension; x++) { + for (size_t y = 0; y < dimension; y++) { + if (x == y) { + if (std::abs(matrix(y, x) - (1.0 + 0.i)) > eps) { + num_found_diagonal++; + if (num_found_diagonal == 1) + top_main_diagonal_coords = x; + if (num_found_diagonal == 2) + bottom_main_diagonal_coords = x; } - if (std::abs(matrix(i, i).real()) < eps && std::abs(matrix(i, i).imag()) < eps) { // maybe use e-6 approx. - if (std::abs(matrix(j, i).real()) < eps && std::abs(matrix(j, i).imag()) < eps) { - continue; - } + } else if (x > y) { // Upper diagonal + if (std::abs(matrix(y, x)) > eps) { + num_upper_triangle_not_zero++; + top_sub_diagonal_row = y; + top_sub_diagonal_col = x; } - - // normalization factor - const double u = std::sqrt(std::norm(matrix(i, i)) + std::norm(matrix(j, i))); - - for (size_t x = 0; x < dimension; x++) { - for (size_t y = 0; y < dimension; y++) { - if (x == y) { - if (x == i) { - conjugate_matrix_product(x, y) = (std::conj(matrix(i, i))) / u; - } else if (x == j) { - conjugate_matrix_product(x, y) = matrix(i, i) / u; - } else { - conjugate_matrix_product(x, y) = 1.0 + 0.i; - } - } else if (x == j && y == i) { - conjugate_matrix_product(x, y) = (-1. + 0.i) * matrix(j, i) / u; - } else if (x == i && y == j) { - conjugate_matrix_product(x, y) = (std::conj(matrix(j, i))) / u; - } else { - conjugate_matrix_product(x, y) = 0. + 0.i; - } - } + } else { // Lower diagonal + if (std::abs(matrix(y, x)) > eps) { + num_lower_triangle_not_zero++; + bottom_sub_diagonal_row = y; + bottom_sub_diagonal_col = x; } - - matrix = tensordot(conjugate_matrix_product, matrix, {1}, {0}); - auto const two_level_kernel = - QTensor({{std::conj(conjugate_matrix_product(i, i)), std::conj(conjugate_matrix_product(j, i))}, - {std::conj(conjugate_matrix_product(i, j)), std::conj(conjugate_matrix_product(j, j))}}); - - two_level_chain.emplace_back(TwoLevelMatrix(two_level_kernel, i, j)); } } - - return two_level_chain; } - /** - * @brief Append gate and save the encoding gate sequence - * - * @param target - * @param qubit_list - * @param gate_list - */ - void encode_control_gate(const QubitIdList target, std::vector& qubit_list, std::vector& gate_list) { - qubit_list.emplace_back(target); - gate_list.emplace_back(target.size() == 2 ? "cx" : "x"); - _quantum_circuit.add_gate(target.size() == 2 ? "cx" : "x", target, {}, true); - } + // is the matrix a two-level one? - /** - * @brief Gray encoder - * - * @param origin_pos Origin qubit - * @param targ_pos Target qubit - * @param qubit_list - * @param gate_list - */ - void encode(size_t origin_pos, size_t targ_pos, std::vector& qubit_list, std::vector& gate_list) { - bool x_given = 0; - if (((origin_pos >> targ_pos) & 1) == 0) { - encode_control_gate({int(targ_pos)}, qubit_list, gate_list); - x_given = 1; + // case 1: some off-diagonal elements are non-zero + if ((num_upper_triangle_not_zero == 1 && num_lower_triangle_not_zero == 1) && (top_sub_diagonal_row == bottom_sub_diagonal_col && top_sub_diagonal_col == bottom_sub_diagonal_row)) { + // kernel is [x x] + // [x x] + if (num_found_diagonal == 2 && top_main_diagonal_coords == top_sub_diagonal_row && bottom_main_diagonal_coords == bottom_sub_diagonal_row) { + return std::make_pair(top_main_diagonal_coords, bottom_main_diagonal_coords); } - for (size_t i = 0; i < _qreg; i++) { - if (i == targ_pos) continue; - if (((origin_pos >> i) & 1) == 0) - encode_control_gate({int(targ_pos), int(i)}, qubit_list, gate_list); + // REVIEW - why is this condition necessary? + if (num_found_diagonal == 1 && (top_main_diagonal_coords == top_sub_diagonal_row || top_main_diagonal_coords == top_sub_diagonal_col)) { + if (top_main_diagonal_coords != dimension - 1) { + return std::make_pair(top_main_diagonal_coords, top_main_diagonal_coords + 1); + } else { + return std::make_pair(top_main_diagonal_coords - 1, top_main_diagonal_coords); + } + } + // kernel is [0 x] + // [x 0] + if (num_found_diagonal == 0) { + return std::make_pair(top_sub_diagonal_row, top_sub_diagonal_col); } - if (x_given) - encode_control_gate({int(targ_pos)}, qubit_list, gate_list); } - /** - * @brief Perform Graycode synthesis - * - * @tparam U - * @param matrix - * @param I - * @param J - * @return true - * @return false - */ - template - bool graycode(Tensor const& matrix, size_t I, size_t J) { - // do pabbing - std::vector qubit_list; - std::vector gate_list; - - size_t diff_pos = 0; - for (size_t i = 0; i < _qreg; i++) { - if ((((I ^ J) >> i) & 1) && (J >> i & 1)) { - diff_pos = i; - break; - } + // case 2: all off-diagonal elements are zero + if (num_upper_triangle_not_zero == 0 && num_lower_triangle_not_zero == 0) { + if (num_found_diagonal == 2) { + return std::make_pair(top_main_diagonal_coords, bottom_main_diagonal_coords); } - - if ((I + size_t(std::pow(2, diff_pos))) != size_t(std::pow(2, _qreg) - 1)) - encode(I, diff_pos, qubit_list, gate_list); - - encode(J, diff_pos, qubit_list, gate_list); - - // decompose CnU - size_t ctrl_index = 0; // q2 q1 q0 = t,c,c -> ctrl_index = 011 = 3 - for (size_t i = 0; i < _qreg; i++) { - if (i != diff_pos) - ctrl_index += size_t(pow(2, i)); + // REVIEW - why is this condition necessary? + if (num_found_diagonal == 1) { + if (top_main_diagonal_coords != dimension - 1) { + return std::make_pair(top_main_diagonal_coords, top_main_diagonal_coords + 1); + } else { + return std::make_pair(top_main_diagonal_coords - 1, top_main_diagonal_coords); + } } - if (!decompose_CnU(matrix, diff_pos, ctrl_index, _qreg - 1)) return false; + return std::make_pair(SIZE_MAX, SIZE_MAX); // signals identity + // if (num_found_diagonal == 0) // identity + // return two_level_chain; + } - // do unpabbing - DVLAB_ASSERT(gate_list.size() == qubit_list.size(), "Sizes of gate list and qubit list are different"); - for (auto const& i : std::views::iota(0UL, gate_list.size()) | std::views::reverse) - _quantum_circuit.add_gate(gate_list[i], qubit_list[i], {}, true); + return std::nullopt; +} + +/** + * @brief Get the two-level matrices associated with the input matrix + * + * @tparam U + * @param matrix + * @return std::vector> List of two-level matrix + * @reference Li, Chi-Kwong, Rebecca Roberts, and Xiaoyan Yin. "Decomposition of unitary matrices and quantum gates." International Journal of Quantum Information 11.01 (2013): 1350015. + */ +template +std::vector> Decomposer::get_two_level_matrices(QTensor matrix /* copy on purpose */) { + using namespace std::literals; + constexpr double eps = 1e-6; + std::vector> two_level_chain; + auto const dimension = static_cast(matrix.shape()[0]); + + for (size_t i = 0; i < dimension; i++) { + for (size_t j = i + 1; j < dimension; j++) { + // if `matrix` is the last two-level matrix + if (auto const pair = get_two_level_matrix_indices(matrix, eps)) { + auto const& [selected_top, selected_bottom] = *pair; + + // shortcut for identity + if (selected_top == SIZE_MAX && selected_bottom == SIZE_MAX) { + return two_level_chain; + } - return true; - } + two_level_chain.emplace_back(make_two_level_matrix(matrix, selected_top, selected_bottom)); + return two_level_chain; + } - /** - * @brief Decompose the CnU gate - * - * @tparam U - * @param t - * @param diff_pos - * @param index - * @param ctrl_gates - * @return true - * @return false - * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. - */ - template - bool decompose_CnU(Tensor const& t, size_t diff_pos, size_t index, size_t ctrl_gates) { - DVLAB_ASSERT(ctrl_gates >= 1, "The control qubit left in the CnU gate should be at least 1"); - size_t ctrl = (diff_pos == 0) ? 1 : diff_pos - 1; + // not a two-level matrix - if (!((index >> ctrl) & 1)) { - for (size_t i = 0; i < _qreg; i++) { - if ((i == diff_pos) || (i == ctrl)) { + // if encounter additional zeros in the off-diagonal elements, skip + if (std::abs(matrix(i, i).real() - 1) < eps && std::abs(matrix(i, i).imag()) < eps) { // maybe use e-6 approx. + if (std::abs(matrix(j, i).real()) < eps && std::abs(matrix(j, i).imag()) < eps) { continue; - } else if ((index >> i) & 1) { - ctrl = i; - break; } } - } - if (ctrl_gates == 1) { - if (!decompose_CU(t, ctrl, diff_pos)) return false; - } else { - size_t extract_qubit = -1; - for (size_t i = 0; i < _qreg; i++) { - if (i == ctrl) continue; - - if ((index >> i) & 1) { - extract_qubit = i; - index = index - size_t(pow(2, i)); - break; + if (std::abs(matrix(i, i).real()) < eps && std::abs(matrix(i, i).imag()) < eps) { // maybe use e-6 approx. + if (std::abs(matrix(j, i).real()) < eps && std::abs(matrix(j, i).imag()) < eps) { + continue; } } - Tensor V = sqrt_single_qubit_matrix(t); - if (!decompose_CU(V, extract_qubit, diff_pos)) return false; - std::vector ctrls; - for (size_t i = 0; i < size_t(log2(index)) + 1; i++) { - if ((index >> i) & 1) - ctrls.emplace_back(i); - } + // normalization factor + const double u = std::sqrt(std::norm(matrix(i, i)) + std::norm(matrix(j, i))); - decompose_CnX(ctrls, extract_qubit, index, ctrl_gates - 1); + auto const n_qubits = static_cast(std::round(std::log2(get_dimension(matrix)))); + QTensor conjugate_matrix_product = + QTensor::identity(n_qubits).to_matrix( + std::views::iota(0ul, n_qubits) | + std::views::transform([](auto i) { return 2 * i + 1; }) | + tl::to(), + std::views::iota(0ul, n_qubits) | + std::views::transform([](auto i) { return 2 * i; }) | + tl::to()); - V.adjoint(); - if (!decompose_CU(V, extract_qubit, diff_pos)) return false; + conjugate_matrix_product(i, i) = std::conj(matrix(i, i)) / u; + conjugate_matrix_product(j, j) = matrix(i, i) / u; - decompose_CnX(ctrls, extract_qubit, index, ctrl_gates - 1); + conjugate_matrix_product(i, j) = std::conj(matrix(j, i)) / u; + conjugate_matrix_product(j, i) = -matrix(j, i) / u; - V.adjoint(); - if (!decompose_CnU(V, diff_pos, index, ctrl_gates - 1)) return false; - } + matrix = tensordot(conjugate_matrix_product, matrix, {1}, {0}); - return true; + two_level_chain.emplace_back(adjoint(make_two_level_matrix(conjugate_matrix_product, i, j))); + } } - /** - * @brief Decompose CnX gate - * - * @tparam U - * @param ctrls - * @param extract_qubit - * @param index - * @param ctrl_gates - * @return true - * @return false - */ - template - bool decompose_CnX(const std::vector& ctrls, const size_t extract_qubit, const size_t index, const size_t ctrl_gates) { - if (ctrls.size() == 1) { - _quantum_circuit.add_gate("cx", {int(ctrls[0]), int(extract_qubit)}, {}, true); - } else if (ctrls.size() == 2) { - _quantum_circuit.add_gate("ccx", {int(ctrls[0]), int(ctrls[1]), int(extract_qubit)}, {}, true); - } else { - using float_type = U::value_type; - if (!decompose_CnU(QTensor::xgate(), extract_qubit, index, ctrl_gates)) return false; + return two_level_chain; +} + +/** + * @brief Perform Graycode synthesis + * + * @tparam U + * @param matrix + * @param I + * @param J + * @return true + * @return false + */ +template +bool Decomposer::Decomposer::graycode(Tensor const& matrix, size_t I, size_t J) { + // do pabbing + std::vector qubit_list; + std::vector gate_list; + + size_t diff_pos = 0; + for (size_t i = 0; i < _n_qubits; i++) { + if ((((I ^ J) >> i) & 1) && (J >> i & 1)) { + diff_pos = i; + break; } - return true; } - /** - * @brief Decompose CU gate - * - * @tparam U - * @param t - * @param ctrl - * @param targ - * @return true - * @return false - * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. - */ - template - bool decompose_CU(Tensor const& t, size_t ctrl, size_t targ) { - constexpr double eps = 1e-6; - const ZYZ angles = decompose_ZYZ(t); - if (!angles.correct) return false; - - if (std::abs((angles.alpha - angles.gamma) / 2) > eps) - _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha - angles.gamma) / 2) * (-1.0)}, true); + if ((I + size_t(std::pow(2, diff_pos))) != size_t(std::pow(2, _n_qubits) - 1)) + encode(I, diff_pos, qubit_list, gate_list); - if (std::abs(angles.beta) > eps) { - _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); - if (std::abs((angles.alpha + angles.gamma) / 2) > eps) - _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha + angles.gamma) / 2) * (-1.0)}, true); + encode(J, diff_pos, qubit_list, gate_list); - _quantum_circuit.add_gate("ry", {int(targ)}, dvlab::Phase{angles.beta * (-1.0)}, true); - _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); - _quantum_circuit.add_gate("ry", {int(targ)}, dvlab::Phase{angles.beta}, true); - - if (std::abs(angles.alpha) > eps) - _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{angles.alpha}, true); - - } else { - if (std::abs((angles.alpha + angles.gamma) / 2) > eps) { - _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); - _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha + angles.gamma) / 2) * (-1.0)}, true); - _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); + // decompose CnU + size_t ctrl_index = 0; // q2 q1 q0 = t,c,c -> ctrl_index = 011 = 3 + for (size_t i = 0; i < _n_qubits; i++) { + if (i != diff_pos) + ctrl_index += size_t(pow(2, i)); + } + if (!decompose_CnU(matrix, diff_pos, ctrl_index, _n_qubits - 1)) return false; + + // do unpabbing + DVLAB_ASSERT(gate_list.size() == qubit_list.size(), "Sizes of gate list and qubit list are different"); + for (auto const& i : std::views::iota(0UL, gate_list.size()) | std::views::reverse) + _quantum_circuit.add_gate(gate_list[i], qubit_list[i], {}, true); + + return true; +} + +/** + * @brief Decompose the CnU gate + * + * @tparam U + * @param t + * @param diff_pos + * @param index + * @param ctrl_gates + * @return true + * @return false + * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. + */ +template +bool Decomposer::decompose_CnU(Tensor const& t, size_t diff_pos, size_t index, size_t ctrl_gates) { + DVLAB_ASSERT(ctrl_gates >= 1, "The control qubit left in the CnU gate should be at least 1"); + size_t ctrl = (diff_pos == 0) ? 1 : diff_pos - 1; + + if (!((index >> ctrl) & 1)) { + for (size_t i = 0; i < _n_qubits; i++) { + if ((i == diff_pos) || (i == ctrl)) { + continue; + } else if ((index >> i) & 1) { + ctrl = i; + break; } - if (std::abs(angles.alpha) > eps) - _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{angles.alpha}, true); } - if (std::abs(angles.phi) > eps) - _quantum_circuit.add_gate("rz", {int(ctrl)}, dvlab::Phase{angles.phi}, true); - - return true; } + if (ctrl_gates == 1) { + if (!decompose_CU(t, ctrl, diff_pos)) return false; + } else { + size_t extract_qubit = -1; + for (size_t i = 0; i < _n_qubits; i++) { + if (i == ctrl) continue; + + if ((index >> i) & 1) { + extract_qubit = i; + index = index - size_t(pow(2, i)); + break; + } + } + Tensor V = sqrt_single_qubit_matrix(t); + if (!decompose_CU(V, extract_qubit, diff_pos)) return false; - /** - * @brief Decompose 2 by 2 matrix into RZ RY RZ gates - * - * @tparam U - * @param matrix - * @return ZYZ - * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. - */ - template - ZYZ decompose_ZYZ(Tensor const& matrix) { - DVLAB_ASSERT(matrix.shape()[0] == 2 && matrix.shape()[1] == 2, "decompose_ZYZ only supports 2x2 matrix"); - using namespace std::literals; - // NOTE - // e^(iφαβγ) - // a = e^{iφ}e^{-i(α+γ)/2}cos(β/2) - // b = -e^{iφ}e^{-i(α-γ)/2}sin(β/2) - // c = e^{iφ}e^{ i(α-γ)/2}sin(β/2) - // d = e^{iφ}e^{ i(α+γ)/2}cos(β/2) - const std::complex a = matrix(0, 0), b = matrix(0, 1), c = matrix(1, 0), d = matrix(1, 1); - ZYZ output = {}; - // NOTE - The beta here is actually half of beta - double init_beta = 0; - if (std::abs(a) > 1) { - init_beta = 0; - } else { - init_beta = std::acos(std::abs(a)); + std::vector ctrls; + for (size_t i = 0; i < size_t(log2(index)) + 1; i++) { + if ((index >> i) & 1) + ctrls.emplace_back(i); } - // NOTE - Possible betas due to arccosine - const std::array beta_candidate = {init_beta, PI - init_beta, PI + init_beta, 2.0 * PI - init_beta}; - for (const auto& beta : beta_candidate) { - output.beta = beta; - std::complex a1, b1, c1, d1; - const std::complex cos(std::cos(beta) + 1e-5, 0); // cos(β/2) - const std::complex sin(std::sin(beta) + 1e-5, 0); // sin(β/2) - a1 = a / cos; - b1 = b / sin; - c1 = c / sin; - d1 = d / cos; - if (std::abs(b) < 1e-4) { - output.alpha = std::arg(d1 / a1) / 2.0; - output.gamma = output.alpha; - } else if (std::abs(a) < 1e-4) { - output.alpha = std::arg(-c1 / b1) / 2.0; - output.gamma = (-1.0) * output.alpha; - } else { - output.alpha = std::arg(c1 / a1); - output.gamma = std::arg(d1 / c1); - } + decompose_CnX(ctrls, extract_qubit, index, ctrl_gates - 1); - auto const alpha_plus_gamma = std::exp(std::complex((0.5i) * (output.alpha + output.gamma))); - auto const alpha_minus_gamma = std::exp(std::complex((0.5i) * (output.alpha - output.gamma))); + V.adjoint(); + if (!decompose_CU(V, extract_qubit, diff_pos)) return false; - if (std::abs(a) < 1e-4) - output.phi = std::arg(c1 / alpha_minus_gamma); - else - output.phi = std::arg(a1 * alpha_plus_gamma); + decompose_CnX(ctrls, extract_qubit, index, ctrl_gates - 1); - const std::complex phi(std::cos(output.phi), std::sin(output.phi)); + V.adjoint(); + if (!decompose_CnU(V, diff_pos, index, ctrl_gates - 1)) return false; + } - if (std::abs(phi * cos / alpha_plus_gamma - a) < 1e-3 && - std::abs(sin * phi / alpha_minus_gamma + b) < 1e-3 && - std::abs(phi * alpha_minus_gamma * sin - c) < 1e-3 && - std::abs(phi * alpha_plus_gamma * cos - d) < 1e-3) { - return output; - } + return true; +} + +/** + * @brief Decompose CnX gate + * + * @tparam U + * @param ctrls + * @param extract_qubit + * @param index + * @param ctrl_gates + * @return true + * @return false + */ +template +bool Decomposer::decompose_CnX(const std::vector& ctrls, const size_t extract_qubit, const size_t index, const size_t ctrl_gates) { + if (ctrls.size() == 1) { + _quantum_circuit.add_gate("cx", {int(ctrls[0]), int(extract_qubit)}, {}, true); + } else if (ctrls.size() == 2) { + _quantum_circuit.add_gate("ccx", {int(ctrls[0]), int(ctrls[1]), int(extract_qubit)}, {}, true); + } else { + using float_type = U::value_type; + if (!decompose_CnU(QTensor::xgate(), extract_qubit, index, ctrl_gates)) return false; + } + return true; +} + +/** + * @brief Decompose CU gate + * + * @tparam U + * @param t + * @param ctrl + * @param targ + * @return true + * @return false + * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. + */ +template +bool Decomposer::decompose_CU(Tensor const& t, size_t ctrl, size_t targ) { + constexpr double eps = 1e-6; + const ZYZ angles = decompose_ZYZ(t); + if (!angles.correct) return false; + + if (std::abs((angles.alpha - angles.gamma) / 2) > eps) + _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha - angles.gamma) / 2) * (-1.0)}, true); + + if (std::abs(angles.beta) > eps) { + _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); + if (std::abs((angles.alpha + angles.gamma) / 2) > eps) + _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha + angles.gamma) / 2) * (-1.0)}, true); + + _quantum_circuit.add_gate("ry", {int(targ)}, dvlab::Phase{angles.beta * (-1.0)}, true); + _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); + _quantum_circuit.add_gate("ry", {int(targ)}, dvlab::Phase{angles.beta}, true); + + if (std::abs(angles.alpha) > eps) + _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{angles.alpha}, true); + + } else { + if (std::abs((angles.alpha + angles.gamma) / 2) > eps) { + _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); + _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{((angles.alpha + angles.gamma) / 2) * (-1.0)}, true); + _quantum_circuit.add_gate("cx", {int(ctrl), int(targ)}, {}, true); } - output.correct = false; - spdlog::error("No solution to ZYZ decomposition"); - - return output; + if (std::abs(angles.alpha) > eps) + _quantum_circuit.add_gate("rz", {int(targ)}, dvlab::Phase{angles.alpha}, true); + } + if (std::abs(angles.phi) > eps) + _quantum_circuit.add_gate("rz", {int(ctrl)}, dvlab::Phase{angles.phi}, true); + + return true; +} + +/** + * @brief Decompose 2 by 2 matrix into RZ RY RZ gates + * + * @tparam U + * @param matrix + * @return ZYZ + * @reference Nakahara, Mikio, and Tetsuo Ohmi. Quantum computing: from linear algebra to physical realizations. CRC press, 2008. + */ +template +ZYZ Decomposer::decompose_ZYZ(Tensor const& matrix) { + DVLAB_ASSERT(matrix.shape()[0] == 2 && matrix.shape()[1] == 2, "decompose_ZYZ only supports 2x2 matrix"); + using namespace std::literals; + // a = e^{iφ}e^{-i(α+γ)/2}cos(β/2) + // b = -e^{iφ}e^{-i(α-γ)/2}sin(β/2) + // c = e^{iφ}e^{ i(α-γ)/2}sin(β/2) + // d = e^{iφ}e^{ i(α+γ)/2}cos(β/2) + const std::complex a = matrix(0, 0), b = matrix(0, 1), c = matrix(1, 0), d = matrix(1, 1); + ZYZ output = {}; + // NOTE - The beta here is actually half of beta + double init_beta = 0; + if (std::abs(a) > 1) { + init_beta = 0; + } else { + init_beta = std::acos(std::abs(a)); } - /** - * @brief Generate the square root of a 2 by 2 matrix - * - * @tparam U - * @param matrix - * @return Tensor - * @reference https://en.wikipedia.org/wiki/Square_root_of_a_2_by_2_matrix - */ - template - Tensor sqrt_single_qubit_matrix(Tensor const& matrix) { - DVLAB_ASSERT(matrix.shape()[0] == 2 && matrix.shape()[1] == 2, "sqrt_single_qubit_matrix only supports 2x2 matrix"); - // a b - // c d - const std::complex a = matrix(0, 0), b = matrix(0, 1), c = matrix(1, 0), d = matrix(1, 1); - const std::complex tau = a + d, delta = a * d - b * c; - const std::complex s = std::sqrt(delta); - const std::complex t = std::sqrt(tau + 2. * s); - if (std::abs(t) > 0) { - return Tensor({{(a + s) / t, b / t}, {c / t, (d + s) / t}}); + // NOTE - Possible betas due to arccosine + const std::array beta_candidate = {init_beta, PI - init_beta, PI + init_beta, 2.0 * PI - init_beta}; + for (const auto& beta : beta_candidate) { + output.beta = beta; + std::complex a1, b1, c1, d1; + const std::complex cos(std::cos(beta) + 1e-5, 0); // cos(β/2) + const std::complex sin(std::sin(beta) + 1e-5, 0); // sin(β/2) + a1 = a / cos; + b1 = b / sin; + c1 = c / sin; + d1 = d / cos; + if (std::abs(b) < 1e-4) { + output.alpha = std::arg(d1 / a1) / 2.0; + output.gamma = output.alpha; + } else if (std::abs(a) < 1e-4) { + output.alpha = std::arg(-c1 / b1) / 2.0; + output.gamma = (-1.0) * output.alpha; } else { - // Diagonalized matrix - return Tensor({{std::sqrt(a), b}, {c, std::sqrt(d)}}); + output.alpha = std::arg(c1 / a1); + output.gamma = std::arg(d1 / c1); + } + + auto const alpha_plus_gamma = std::exp(std::complex((0.5i) * (output.alpha + output.gamma))); + auto const alpha_minus_gamma = std::exp(std::complex((0.5i) * (output.alpha - output.gamma))); + + if (std::abs(a) < 1e-4) + output.phi = std::arg(c1 / alpha_minus_gamma); + else + output.phi = std::arg(a1 * alpha_plus_gamma); + + const std::complex phi(std::cos(output.phi), std::sin(output.phi)); + + if (std::abs(phi * cos / alpha_plus_gamma - a) < 1e-3 && + std::abs(sin * phi / alpha_minus_gamma + b) < 1e-3 && + std::abs(phi * alpha_minus_gamma * sin - c) < 1e-3 && + std::abs(phi * alpha_plus_gamma * cos - d) < 1e-3) { + return output; } } -}; + output.correct = false; + spdlog::error("No solution to ZYZ decomposition"); + + return output; +} + +/** + * @brief Generate the square root of a 2 by 2 matrix + * + * @tparam U + * @param matrix + * @return Tensor + * @reference https://en.wikipedia.org/wiki/Square_root_of_a_2_by_2_matrix + */ +template +Tensor Decomposer::sqrt_single_qubit_matrix(Tensor const& matrix) { + DVLAB_ASSERT(matrix.shape()[0] == 2 && matrix.shape()[1] == 2, "sqrt_single_qubit_matrix only supports 2x2 matrix"); + // a b + // c d + const std::complex a = matrix(0, 0), b = matrix(0, 1), c = matrix(1, 0), d = matrix(1, 1); + const std::complex tau = a + d, delta = a * d - b * c; + const std::complex s = std::sqrt(delta); + const std::complex t = std::sqrt(tau + 2. * s); + if (std::abs(t) > 0) { + return Tensor({{(a + s) / t, b / t}, {c / t, (d + s) / t}}); + } else { + // Diagonalized matrix + return Tensor({{std::sqrt(a), b}, {c, std::sqrt(d)}}); + } +} } // namespace tensor