Skip to content

✨ Support for Qiskit's IfElseOp #923

Open
@burgholzer

Description

@burgholzer

What's the problem this feature will solve?

It's been a while since Qiskit has introduced "Classical feedforward and control flow", which matches OpenQASM 3's "Looping and branching".

Within the MQT, support for these dynamic circuit primitives is still limited.
We support mid-circuit measurements, reset operations, as well as a limited set of classically-controlled operations, which roughly match the capabilities of OpenQASM 2's if statement (see https://arxiv.org/abs/1707.03429v2). See also the MQT Core IR quickstart guide.

The goal of this issue is to add proper support for Qiskit's IfElseOp to the MQT Core IR so that Qiskit circuits using such instructions can be imported into the MQT, used within it, as well as exported back to Qiskit.
This requires

  • adapting the MQT Core IR to be capable of properly representing an if (...) { ... } else { ... } construct
  • adapting the Qiskit import and export routines to handle the newly-supported operation
  • adapting the OpenQASM3 parser and exporter to handle the newly supported operation.
  • refactoring any dependant code to use the new functionality.

Describe the solution you'd like

Any solution to this issue may take inspiration from the way Qiskit is handling these operations. Specifically, the ControlFlowOp class as well as the IfElseOp class.
It probably makes sense to replicate a similar operation hierarchy in MQT Core.
So instead of

Operation --> ClassicControlledOperation

it is probably reasonable to refactor this to

Operation --> ControlFlowOperation --> IfElseOperation

For the purpose of this PR, it is fine if the IfElseOperation only supports conditions in the form they are currently supported throughout MQT Core, that is

  • either a full classical register is compared to an integer, or
  • a single bit is compared against a Boolean value.

Where to find the respective code?

The classically-controlled operations are currently defined here: https://github.com/munich-quantum-toolkit/core/blob/main/include/mqt-core/ir/operations/ClassicControlledOperation.hpp and contain the following members

std::unique_ptr<Operation> op;
std::optional<ClassicalRegister> controlRegister;
std::optional<Bit> controlBit;
std::uint64_t expectedValue = 1U;
ComparisonKind comparisonKind = Eq;

They are exposed as utility functions in the QuantumComputation class

void classicControlled(OpType op, Qubit target,
const ClassicalRegister& controlRegister,
std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});
void classicControlled(OpType op, Qubit target, Control control,
const ClassicalRegister& controlRegister,
std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});
void classicControlled(OpType op, Qubit target, const Controls& controls,
const ClassicalRegister& controlRegister,
std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});
void classicControlled(OpType op, Qubit target, Bit cBit,
std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});
void classicControlled(OpType op, Qubit target, Control control, Bit cBit,
std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});
void classicControlled(OpType op, Qubit target, const Controls& controls,
Bit cBit, std::uint64_t expectedValue = 1U,
ComparisonKind cmp = Eq,
const std::vector<fp>& params = {});

In a similar fashion, they are exposed to Python

  • void registerClassicControlledOperation(const py::module& m) {
    py::enum_<qc::ComparisonKind>(m, "ComparisonKind")
    .value("eq", qc::ComparisonKind::Eq)
    .value("neq", qc::ComparisonKind::Neq)
    .value("lt", qc::ComparisonKind::Lt)
    .value("leq", qc::ComparisonKind::Leq)
    .value("gt", qc::ComparisonKind::Gt)
    .value("geq", qc::ComparisonKind::Geq)
    .export_values()
    .def("__str__",
    [](const qc::ComparisonKind& cmp) { return qc::toString(cmp); })
    .def("__repr__",
    [](const qc::ComparisonKind& cmp) { return qc::toString(cmp); });
    auto ccop = py::class_<qc::ClassicControlledOperation, qc::Operation>(
    m, "ClassicControlledOperation");
    ccop.def(py::init([](const qc::Operation* operation,
    const qc::ClassicalRegister& controlReg,
    std::uint64_t expectedVal, qc::ComparisonKind cmp) {
    return std::make_unique<qc::ClassicControlledOperation>(
    operation->clone(), controlReg, expectedVal, cmp);
    }),
    "operation"_a, "control_register"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq);
    ccop.def(py::init([](const qc::Operation* operation, qc::Bit cBit,
    std::uint64_t expectedVal, qc::ComparisonKind cmp) {
    return std::make_unique<qc::ClassicControlledOperation>(
    operation->clone(), cBit, expectedVal, cmp);
    }),
    "operation"_a, "control_bit"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq);
    ccop.def_property_readonly("operation",
    &qc::ClassicControlledOperation::getOperation,
    py::return_value_policy::reference_internal);
    ccop.def_property_readonly(
    "control_register", &qc::ClassicControlledOperation::getControlRegister);
    ccop.def_property_readonly("control_bit",
    &qc::ClassicControlledOperation::getControlBit);
    ccop.def_property_readonly("expected_value",
    &qc::ClassicControlledOperation::getExpectedValue);
    ccop.def_property_readonly(
    "comparison_kind", &qc::ClassicControlledOperation::getComparisonKind);
    ccop.def("__repr__", [](const qc::ClassicControlledOperation& op) {
    std::stringstream ss;
    ss << "ClassicControlledOperation(<...op...>, ";
    if (const auto& controlReg = op.getControlRegister();
    controlReg.has_value()) {
    ss << "control_register=ClassicalRegister(" << controlReg->getSize()
    << ", " << controlReg->getStartIndex() << ", " << controlReg->getName()
    << "), ";
    }
    if (const auto& controlBit = op.getControlBit(); controlBit.has_value()) {
    ss << "control_bit=" << controlBit.value() << ", ";
    }
    ss << "expected_value=" << op.getExpectedValue() << ", "
    << "comparison_kind='" << op.getComparisonKind() << "')";
    return ss.str();
    });
    }
  • qc.def(
    "classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit,
    const qc::ClassicalRegister&, const std::uint64_t,
    const qc::ComparisonKind, const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "creg"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});
    qc.def(
    "classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit, const qc::Control,
    const qc::ClassicalRegister&, const std::uint64_t,
    const qc::ComparisonKind, const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "control"_a, "creg"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});
    qc.def(
    "classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit, const qc::Controls&,
    const qc::ClassicalRegister&, const std::uint64_t,
    const qc::ComparisonKind, const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "controls"_a, "creg"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});
    qc.def("classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit, const qc::Bit,
    const std::uint64_t, const qc::ComparisonKind,
    const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "cbit"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});
    qc.def(
    "classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit, const qc::Control,
    const qc::Bit, const std::uint64_t,
    const qc::ComparisonKind, const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "control"_a, "cbit"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});
    qc.def(
    "classic_controlled",
    py::overload_cast<const qc::OpType, const qc::Qubit, const qc::Controls&,
    const qc::Bit, const std::uint64_t,
    const qc::ComparisonKind, const std::vector<qc::fp>&>(
    &qc::QuantumComputation::classicControlled),
    "op"_a, "target"_a, "controls"_a, "cbit"_a, "expected_value"_a = 1U,
    "comparison_kind"_a = qc::ComparisonKind::Eq,
    "params"_a = std::vector<qc::fp>{});

Neither the Qiskit import nor the export handles that type of gate yet:

  • _NATIVELY_SUPPORTED_GATES = frozenset({
    "i",
    "id",
    "iden",
    "x",
    "y",
    "z",
    "h",
    "s",
    "sdg",
    "t",
    "tdg",
    "p",
    "u1",
    "rx",
    "ry",
    "rz",
    "u2",
    "u",
    "u3",
    "cx",
    "cy",
    "cz",
    "cs",
    "csdg",
    "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",
    "reset",
    "barrier",
    "measure",
    })
  • elif isinstance(op, ClassicControlledOperation):
    msg = "Conversion of classic-controlled operations to Qiskit is not yet supported."
    raise NotImplementedError(msg)

The OpenQASM 3 importer breaks down if-else statements into two separate ClassicControlledOperations:

core/src/qasm3/Importer.cpp

Lines 892 to 929 in d88d1f0

void Importer::visitIfStatement(
const std::shared_ptr<IfStatement> ifStatement) {
const auto& condition =
translateCondition(ifStatement->condition, ifStatement->debugInfo);
// translate statements in then/else blocks
if (!ifStatement->thenStatements.empty()) {
auto thenOps = translateBlockOperations(ifStatement->thenStatements);
if (std::holds_alternative<std::pair<qc::Bit, bool>>(condition)) {
const auto& [bit, val] = std::get<std::pair<qc::Bit, bool>>(condition);
qc->emplace_back<qc::ClassicControlledOperation>(std::move(thenOps), bit,
val ? 1 : 0);
} else {
const auto& [creg, comparisonKind, rhs] = std::get<
std::tuple<qc::ClassicalRegister, qc::ComparisonKind, uint64_t>>(
condition);
qc->emplace_back<qc::ClassicControlledOperation>(std::move(thenOps), creg,
rhs, comparisonKind);
}
}
if (!ifStatement->elseStatements.empty()) {
auto elseOps = translateBlockOperations(ifStatement->elseStatements);
if (std::holds_alternative<std::pair<qc::Bit, bool>>(condition)) {
const auto& [bit, val] = std::get<std::pair<qc::Bit, bool>>(condition);
qc->emplace_back<qc::ClassicControlledOperation>(std::move(elseOps), bit,
val ? 0 : 1);
} else {
const auto& [creg, comparisonKind, rhs] = std::get<
std::tuple<qc::ClassicalRegister, qc::ComparisonKind, uint64_t>>(
condition);
const auto invertedComparisonKind =
qc::getInvertedComparisonKind(comparisonKind);
qc->emplace_back<qc::ClassicControlledOperation>(
std::move(elseOps), creg, rhs, invertedComparisonKind);
}
}
}

The corresponding exporter works for the limited scope of the ClassicControlledOperation:
void ClassicControlledOperation::dumpOpenQASM(
std::ostream& of, const QubitIndexToRegisterMap& qubitMap,
const BitIndexToRegisterMap& bitMap, const std::size_t indent,
const bool openQASM3) const {
of << std::string(indent * OUTPUT_INDENT_SIZE, ' ');
of << "if (";
if (controlRegister.has_value()) {
assert(!controlBit.has_value());
of << controlRegister->getName() << " " << comparisonKind << " "
<< expectedValue;
}
if (controlBit.has_value()) {
assert(!controlRegister.has_value());
of << (expectedValue == 0 ? "!" : "") << bitMap.at(*controlBit).second;
}
of << ") ";
if (openQASM3) {
of << "{\n";
}
op->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3);
if (openQASM3) {
of << "}\n";
}
}

Further places where the ClassicControlledOperation class is used can easily be found via code search.

Metadata

Metadata

Assignees

No one assigned

    Labels

    CoreAnything related to the Core library and IRc++Anything related to C++ codefeatureNew feature or requestgood first issueGood for newcomersminorMinor version updaterefactorAnything related to code refactoringunitaryHackIssues and PRs intended for unitaryHack

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions