From 3d5e14b20809108b90de6df03fa25def3d2851c9 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 29 Jan 2024 16:16:00 +0100 Subject: [PATCH] =?UTF-8?q?=E2=8F=AA=20add=20`MQT::CorePython`=20target=20?= =?UTF-8?q?back=20(#541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As the integration of the mqt-core Python package into the top-level tools turned out to be a much lengthier process than imagined, this PR adds back the `MQT::CorePython` target that was previously used to read in Qiskit objects. This should allow development in the top-level libraries to continue without waiting for the remaining issues with the Python package being resolved. ## Checklist: - [x] The pull request only contains commits that are related to it. - [x] I have added appropriate tests and documentation. - [x] I have made sure that all CI jobs on GitHub pass. - [x] The pull request introduces no new warnings and follows the project's style guidelines. --------- Signed-off-by: burgholzer --- .../mqt-core/python/qiskit/QuantumCircuit.hpp | 49 ++ src/CMakeLists.txt | 23 + src/python/qiskit/QuantumCircuit.cpp | 480 ++++++++++++++++++ 3 files changed, 552 insertions(+) create mode 100644 include/mqt-core/python/qiskit/QuantumCircuit.hpp create mode 100644 src/python/qiskit/QuantumCircuit.cpp diff --git a/include/mqt-core/python/qiskit/QuantumCircuit.hpp b/include/mqt-core/python/qiskit/QuantumCircuit.hpp new file mode 100644 index 000000000..7a135ded2 --- /dev/null +++ b/include/mqt-core/python/qiskit/QuantumCircuit.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" + +#include +#include +#include + +namespace py = pybind11; + +#include "QuantumComputation.hpp" + +namespace qc::qiskit { +using namespace pybind11::literals; + +class QuantumCircuit { +public: + static void import(QuantumComputation& qc, const py::object& circ); + +protected: + static void emplaceOperation(QuantumComputation& qc, + const py::object& instruction, + const py::list& qargs, const py::list& cargs, + const py::list& params, const py::dict& qubitMap, + const py::dict& clbitMap); + + static SymbolOrNumber parseSymbolicExpr(const py::object& pyExpr); + + static SymbolOrNumber parseParam(const py::object& param); + + static void addOperation(QuantumComputation& qc, OpType type, + const py::list& qargs, const py::list& params, + const py::dict& qubitMap); + + static void addTwoTargetOperation(QuantumComputation& qc, OpType type, + const py::list& qargs, + const py::list& params, + const py::dict& qubitMap); + + static void importDefinition(QuantumComputation& qc, const py::object& circ, + const py::list& qargs, const py::list& cargs, + const py::dict& qubitMap, + const py::dict& clbitMap); + + static void importInitialLayout(QuantumComputation& qc, + const py::object& circ); +}; +} // namespace qc::qiskit diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e6b5952df..e5aba42d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,29 @@ add_subdirectory(zx) # add ECC library add_subdirectory(ecc) +# ** Note ** The following target will soon be removed from the project. All top-level projects +# should switch to using the mqt-core Python package. +if(BINDINGS AND NOT TARGET mqt-core-python) + # add Python interface library + add_library( + ${MQT_CORE_TARGET_NAME}-python ${MQT_CORE_INCLUDE_BUILD_DIR}/python/qiskit/QuantumCircuit.hpp + python/qiskit/QuantumCircuit.cpp) + + # link with main project library and pybind11 libraries + target_link_libraries(${MQT_CORE_TARGET_NAME}-python PUBLIC MQT::Core pybind11::pybind11) + target_link_libraries(${MQT_CORE_TARGET_NAME}-python PRIVATE MQT::ProjectOptions + MQT::ProjectWarnings) + + # add MQT alias + add_library(MQT::CorePython ALIAS ${MQT_CORE_TARGET_NAME}-python) + set_target_properties( + ${MQT_CORE_TARGET_NAME}-python + PROPERTIES VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + EXPORT_NAME CorePython) + list(APPEND MQT_CORE_TARGETS ${MQT_CORE_TARGET_NAME}-python) +endif() + if(BUILD_MQT_CORE_BINDINGS) add_subdirectory(python) endif() diff --git a/src/python/qiskit/QuantumCircuit.cpp b/src/python/qiskit/QuantumCircuit.cpp new file mode 100644 index 000000000..54fba1b2a --- /dev/null +++ b/src/python/qiskit/QuantumCircuit.cpp @@ -0,0 +1,480 @@ +#include "python/qiskit/QuantumCircuit.hpp" + +void qc::qiskit::QuantumCircuit::import(qc::QuantumComputation& qc, + const py::object& circ) { + qc.reset(); + + const py::object quantumCircuit = + py::module::import("qiskit").attr("QuantumCircuit"); + + if (!py::isinstance(circ, quantumCircuit)) { + throw QFRException( + "[import] Python object needs to be a Qiskit QuantumCircuit"); + } + + if (!circ.attr("name").is_none()) { + qc.setName(circ.attr("name").cast()); + } + + // handle qubit registers + const py::object qubit = py::module::import("qiskit.circuit").attr("Qubit"); + const py::object ancillaQubit = + py::module::import("qiskit.circuit").attr("AncillaQubit"); + const py::object ancillaRegister = + py::module::import("qiskit.circuit").attr("AncillaRegister"); + int qubitIndex = 0; + const py::dict qubitMap{}; + auto&& circQregs = circ.attr("qregs"); + for (const auto qreg : circQregs) { + // create corresponding register in quantum computation + auto size = qreg.attr("size").cast(); + auto name = qreg.attr("name").cast(); + if (py::isinstance(qreg, ancillaRegister)) { + qc.addAncillaryRegister(size, name); + // add ancillas to qubit map + for (std::size_t i = 0; i < size; ++i) { + qubitMap[ancillaQubit(qreg, i)] = qubitIndex; + qubitIndex++; + } + } else { + qc.addQubitRegister(size, name); + // add qubits to qubit map + for (std::size_t i = 0; i < size; ++i) { + qubitMap[qubit(qreg, i)] = qubitIndex; + qubitIndex++; + } + } + } + + // handle classical registers + const py::object clbit = py::module::import("qiskit.circuit").attr("Clbit"); + int clbitIndex = 0; + const py::dict clbitMap{}; + auto&& circCregs = circ.attr("cregs"); + for (const auto creg : circCregs) { + // create corresponding register in quantum computation + auto size = creg.attr("size").cast(); + auto name = creg.attr("name").cast(); + qc.addClassicalRegister(size, name); + + // add clbits to clbit map + for (std::size_t i = 0; i < size; ++i) { + clbitMap[clbit(creg, i)] = clbitIndex; + clbitIndex++; + } + } + + try { + qc.gphase(circ.attr("global_phase").cast()); + } catch (const py::cast_error& e) { + std::clog << e.what() << "\n"; + std::clog << "[import] Warning: Symbolic global phase values are not " + "supported yet. Ignoring global phase.\n"; + } + + // iterate over instructions + auto&& data = circ.attr("data"); + for (const auto pyinst : data) { + auto&& inst = pyinst.cast>(); + auto&& instruction = std::get<0>(inst); + auto&& qargs = std::get<1>(inst); + auto&& cargs = std::get<2>(inst); + auto&& params = instruction.attr("params"); + + emplaceOperation(qc, instruction, qargs, cargs, params, qubitMap, clbitMap); + } + + // import initial layout in case it is available + if (!circ.attr("_layout").is_none()) { + importInitialLayout(qc, circ); + } + qc.initializeIOMapping(); +} + +void qc::qiskit::QuantumCircuit::emplaceOperation( + qc::QuantumComputation& qc, const py::object& instruction, + const py::list& qargs, const py::list& cargs, const py::list& params, + const py::dict& qubitMap, const py::dict& clbitMap) { + static const auto NATIVELY_SUPPORTED_GATES = + std::set{"i", "id", "iden", + "x", "y", "z", + "h", "s", "sdg", + "t", "tdg", "p", + "u1", "rx", "ry", + "rz", "u2", "u", + "u3", "cx", "cy", + "cz", "cp", "cu1", + "ch", "crx", "cry", + "crz", "cu3", "ccx", + "swap", "cswap", "iswap", + "sx", "sxdg", "csx", + "mcx", "mcx_gray", "mcx_recursive", + "mcx_vchain", "mcphase", "mcrx", + "mcry", "mcrz", "dcx", + "ecr", "rxx", "ryy", + "rzx", "rzz", "xx_minus_yy", + "xx_plus_yy"}; + + auto instructionName = instruction.attr("name").cast(); + if (instructionName == "measure") { + auto control = qubitMap[qargs[0]].cast(); + auto target = clbitMap[cargs[0]].cast(); + qc.emplace_back(qc.getNqubits(), control, target); + } else if (instructionName == "barrier") { + Targets targets{}; + for (const auto qubit : qargs) { + auto target = qubitMap[qubit].cast(); + targets.emplace_back(target); + } + qc.emplace_back(qc.getNqubits(), targets, Barrier); + } else if (instructionName == "reset") { + Targets targets{}; + for (const auto qubit : qargs) { + auto target = qubitMap[qubit].cast(); + targets.emplace_back(target); + } + qc.reset(targets); + } else if (NATIVELY_SUPPORTED_GATES.count(instructionName) != 0) { + // natively supported operations + if (instructionName == "i" || instructionName == "id" || + instructionName == "iden") { + addOperation(qc, I, qargs, params, qubitMap); + } else if (instructionName == "x" || instructionName == "cx" || + instructionName == "ccx" || instructionName == "mcx_gray" || + instructionName == "mcx") { + addOperation(qc, X, qargs, params, qubitMap); + } else if (instructionName == "y" || instructionName == "cy") { + addOperation(qc, Y, qargs, params, qubitMap); + } else if (instructionName == "z" || instructionName == "cz") { + addOperation(qc, Z, qargs, params, qubitMap); + } else if (instructionName == "h" || instructionName == "ch") { + addOperation(qc, H, qargs, params, qubitMap); + } else if (instructionName == "s") { + addOperation(qc, S, qargs, params, qubitMap); + } else if (instructionName == "sdg") { + addOperation(qc, Sdg, qargs, params, qubitMap); + } else if (instructionName == "t") { + addOperation(qc, T, qargs, params, qubitMap); + } else if (instructionName == "tdg") { + addOperation(qc, Tdg, qargs, params, qubitMap); + } else if (instructionName == "rx" || instructionName == "crx" || + instructionName == "mcrx") { + addOperation(qc, RX, qargs, params, qubitMap); + } else if (instructionName == "ry" || instructionName == "cry" || + instructionName == "mcry") { + addOperation(qc, RY, qargs, params, qubitMap); + } else if (instructionName == "rz" || instructionName == "crz" || + instructionName == "mcrz") { + addOperation(qc, RZ, qargs, params, qubitMap); + } else if (instructionName == "p" || instructionName == "u1" || + instructionName == "cp" || instructionName == "cu1" || + instructionName == "mcphase") { + addOperation(qc, P, qargs, params, qubitMap); + } else if (instructionName == "sx" || instructionName == "csx") { + addOperation(qc, SX, qargs, params, qubitMap); + } else if (instructionName == "sxdg") { + addOperation(qc, SXdg, qargs, params, qubitMap); + } else if (instructionName == "u2") { + addOperation(qc, U2, qargs, params, qubitMap); + } else if (instructionName == "u" || instructionName == "u3" || + instructionName == "cu3") { + addOperation(qc, U, qargs, params, qubitMap); + } else if (instructionName == "swap" || instructionName == "cswap") { + addTwoTargetOperation(qc, SWAP, qargs, params, qubitMap); + } else if (instructionName == "iswap") { + addTwoTargetOperation(qc, iSWAP, qargs, params, qubitMap); + } else if (instructionName == "dcx") { + addTwoTargetOperation(qc, DCX, qargs, params, qubitMap); + } else if (instructionName == "ecr") { + addTwoTargetOperation(qc, ECR, qargs, params, qubitMap); + } else if (instructionName == "rxx") { + addTwoTargetOperation(qc, RXX, qargs, params, qubitMap); + } else if (instructionName == "ryy") { + addTwoTargetOperation(qc, RYY, qargs, params, qubitMap); + } else if (instructionName == "rzx") { + addTwoTargetOperation(qc, RZX, qargs, params, qubitMap); + } else if (instructionName == "rzz") { + addTwoTargetOperation(qc, RZZ, qargs, params, qubitMap); + } else if (instructionName == "xx_minus_yy") { + addTwoTargetOperation(qc, XXminusYY, qargs, params, qubitMap); + } else if (instructionName == "xx_plus_yy") { + addTwoTargetOperation(qc, XXplusYY, qargs, params, qubitMap); + } else if (instructionName == "mcx_recursive") { + if (qargs.size() <= 5) { + addOperation(qc, X, qargs, params, qubitMap); + } else { + auto qargsCopy = qargs.attr("copy")(); + qargsCopy.attr("pop")(); // discard ancillaries + addOperation(qc, X, qargsCopy, params, qubitMap); + } + } else if (instructionName == "mcx_vchain") { + auto size = qargs.size(); + const std::size_t ncontrols = (size + 1) / 2; + auto qargsCopy = qargs.attr("copy")(); + // discard ancillaries + for (std::size_t i = 0; i < ncontrols - 2; ++i) { + qargsCopy.attr("pop")(); + } + addOperation(qc, X, qargsCopy, params, qubitMap); + } + } else { + try { + importDefinition(qc, instruction.attr("definition"), qargs, cargs, + qubitMap, clbitMap); + } catch (py::error_already_set& e) { + std::cerr << "Failed to import instruction " << instructionName + << " from Qiskit QuantumCircuit" << std::endl; + std::cerr << e.what() << std::endl; + } + } +} + +qc::SymbolOrNumber +qc::qiskit::QuantumCircuit::parseSymbolicExpr(const py::object& pyExpr) { + static const std::regex SUMMANDS("[+|-]?[^+-]+"); + static const std::regex PRODUCTS("[\\*/]?[^\\*/]+"); + + auto exprStr = pyExpr.attr("__str__")().cast(); + exprStr.erase(std::remove(exprStr.begin(), exprStr.end(), ' '), + exprStr.end()); // strip whitespace + + auto sumIt = std::sregex_iterator(exprStr.begin(), exprStr.end(), SUMMANDS); + const auto sumEnd = std::sregex_iterator(); + + qc::Symbolic sym; + bool isConst = true; + + while (sumIt != sumEnd) { + auto match = *sumIt; + auto matchStr = match.str(); + const int sign = matchStr[0] == '-' ? -1 : 1; + if (matchStr[0] == '+' || matchStr[0] == '-') { + matchStr.erase(0, 1); + } + + auto prodIt = + std::sregex_iterator(matchStr.begin(), matchStr.end(), PRODUCTS); + auto prodEnd = std::sregex_iterator(); + + fp coeff = 1.0; + std::string var; + while (prodIt != prodEnd) { + auto prodMatch = *prodIt; + auto prodStr = prodMatch.str(); + + const bool isDiv = prodStr[0] == '/'; + if (prodStr[0] == '*' || prodStr[0] == '/') { + prodStr.erase(0, 1); + } + + std::istringstream iss(prodStr); + fp f{}; + iss >> f; + + if (iss.eof() && !iss.fail()) { + coeff *= isDiv ? 1.0 / f : f; + } else { + var = prodStr; + } + + ++prodIt; + } + if (var.empty()) { + sym += coeff; + } else { + isConst = false; + sym += sym::Term(sym::Variable{var}, sign * coeff); + } + ++sumIt; + } + + if (isConst) { + return {sym.getConst()}; + } + return {sym}; +} + +qc::SymbolOrNumber +qc::qiskit::QuantumCircuit::parseParam(const py::object& param) { + try { + return param.cast(); + } catch ([[maybe_unused]] py::cast_error& e) { + return parseSymbolicExpr(param); + } +} + +void qc::qiskit::QuantumCircuit::addOperation(qc::QuantumComputation& qc, + qc::OpType type, + const py::list& qargs, + const py::list& params, + const py::dict& qubitMap) { + std::vector qubits{}; + for (const auto qubit : qargs) { + auto target = qubitMap[qubit].cast(); + qubits.emplace_back(Control{target}); + } + auto target = qubits.back().qubit; + qubits.pop_back(); + std::vector parameters{}; + for (const auto& param : params) { + parameters.emplace_back( + parseParam(py::reinterpret_borrow(param))); + } + const Controls controls(qubits.cbegin(), qubits.cend()); + if (std::all_of(parameters.cbegin(), parameters.cend(), [](const auto& p) { + return std::holds_alternative(p); + })) { + std::vector fpParams{}; + std::transform(parameters.cbegin(), parameters.cend(), + std::back_inserter(fpParams), + [](const auto& p) { return std::get(p); }); + qc.emplace_back(qc.getNqubits(), controls, target, type, + fpParams); + } else { + qc.emplace_back(qc.getNqubits(), controls, target, type, + parameters); + for (const auto& p : parameters) { + qc.addVariables(p); + } + } +} + +void qc::qiskit::QuantumCircuit::addTwoTargetOperation( + qc::QuantumComputation& qc, qc::OpType type, const py::list& qargs, + const py::list& params, const py::dict& qubitMap) { + std::vector qubits{}; + for (const auto qubit : qargs) { + auto target = qubitMap[qubit].cast(); + qubits.emplace_back(Control{target}); + } + auto target1 = qubits.back().qubit; + qubits.pop_back(); + auto target0 = qubits.back().qubit; + qubits.pop_back(); + std::vector parameters{}; + for (const auto& param : params) { + parameters.emplace_back( + parseParam(py::reinterpret_borrow(param))); + } + const Controls controls(qubits.cbegin(), qubits.cend()); + if (std::all_of(parameters.cbegin(), parameters.cend(), [](const auto& p) { + return std::holds_alternative(p); + })) { + std::vector fpParams{}; + std::transform(parameters.cbegin(), parameters.cend(), + std::back_inserter(fpParams), + [](const auto& p) { return std::get(p); }); + qc.emplace_back(qc.getNqubits(), controls, target0, + target1, type, fpParams); + } else { + qc.emplace_back(qc.getNqubits(), controls, target0, + target1, type, parameters); + for (const auto& p : parameters) { + qc.addVariables(p); + } + } +} + +void qc::qiskit::QuantumCircuit::importDefinition( + qc::QuantumComputation& qc, const py::object& circ, const py::list& qargs, + const py::list& cargs, const py::dict& qubitMap, const py::dict& clbitMap) { + const py::dict qargMap{}; + py::list&& defQubits = circ.attr("qubits"); + for (size_t i = 0; i < qargs.size(); ++i) { + qargMap[defQubits[i]] = qargs[i]; + } + + const py::dict cargMap{}; + py::list&& defClbits = circ.attr("clbits"); + for (size_t i = 0; i < cargs.size(); ++i) { + cargMap[defClbits[i]] = cargs[i]; + } + + auto&& data = circ.attr("data"); + for (const auto pyinst : data) { + auto&& inst = pyinst.cast>(); + auto&& instruction = std::get<0>(inst); + + const py::list& instQargs = std::get<1>(inst); + py::list mappedQargs{}; + for (auto&& instQarg : instQargs) { + mappedQargs.append(qargMap[instQarg]); + } + + const py::list& instCargs = std::get<2>(inst); + py::list mappedCargs{}; + for (auto&& instCarg : instCargs) { + mappedCargs.append(cargMap[instCarg]); + } + + auto&& instParams = instruction.attr("params"); + + emplaceOperation(qc, instruction, mappedQargs, mappedCargs, instParams, + qubitMap, clbitMap); + } +} + +void qc::qiskit::QuantumCircuit::importInitialLayout(qc::QuantumComputation& qc, + const py::object& circ) { + const py::object qubit = py::module::import("qiskit.circuit").attr("Qubit"); + + // get layout + auto layout = circ.attr("_layout"); + + // qiskit-terra 0.22.0 changed the `_layout` attribute to a + // `TranspileLayout` dataclass object that contains the initial layout as a + // `Layout` object in the `initial_layout` attribute. + if (py::hasattr(layout, "initial_layout")) { + layout = layout.attr("initial_layout"); + } + + // create map between registers used in the layout and logical qubit indices + // NOTE: this only works correctly if the registers were originally declared + // in alphabetical order! + const auto registers = layout.attr("get_registers")().cast(); + std::size_t logicalQubitIndex = 0U; + const py::dict logicalQubitIndices{}; + + // the ancilla register + decltype(registers.get_type()) ancillaRegister = py::none(); + + for (const auto qreg : registers) { + // skip ancillary register since it is handled as the very last qubit + // register + if (const auto qregName = qreg.attr("name").cast(); + qregName == "ancilla") { + ancillaRegister = qreg; + continue; + } + + const auto size = qreg.attr("size").cast(); + for (std::size_t i = 0U; i < size; ++i) { + logicalQubitIndices[qubit(qreg, i)] = logicalQubitIndex; + ++logicalQubitIndex; + } + } + + // handle ancillary register, if there is one + if (!ancillaRegister.is_none()) { + const auto size = ancillaRegister.attr("size").cast(); + for (std::size_t i = 0U; i < size; ++i) { + logicalQubitIndices[qubit(ancillaRegister, i)] = logicalQubitIndex; + qc.setLogicalQubitAncillary(static_cast(logicalQubitIndex)); + ++logicalQubitIndex; + } + } + + // get a map of physical to logical qubits + const auto physicalQubits = + layout.attr("get_physical_bits")().cast(); + + // create initial layout (and assume identical output permutation) + for (const auto& [physicalQubit, logicalQubit] : physicalQubits) { + if (logicalQubitIndices.contains(logicalQubit)) { + qc.initialLayout[physicalQubit.cast()] = + logicalQubitIndices[logicalQubit].cast(); + qc.outputPermutation[physicalQubit.cast()] = + logicalQubitIndices[logicalQubit].cast(); + } + } +}