Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2134c2e
Add multi-control functionality
Dec 8, 2025
0ae2b61
Merge branch 'main' into multi_control
keefehuang Dec 8, 2025
f8c4503
Apply minor updates to code
Dec 8, 2025
894add1
Remove old CCX test
Dec 8, 2025
fb32afb
Add tests for CCZ, CRZ, MCRZ and MCZ
Dec 8, 2025
2375b74
Rename control and target for addCrz
Dec 8, 2025
275e832
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 8, 2025
c873318
:sparkles: Qiskit to IQM JSON converter
marcelwa Dec 7, 2025
068560c
:white_check_mark: Test Qiskit to IQM JSON converter
marcelwa Dec 7, 2025
53032bb
:sparkles: Support IQM JSON conversion in `QDMIBackend`
marcelwa Dec 7, 2025
5c68a2f
:white_check_mark: Tests for new backend functionality
marcelwa Dec 7, 2025
3f46cd3
:art: Apply suggestions from GitHub Copilot
marcelwa Dec 7, 2025
33b48a7
:art: Apply `pre-commit`
marcelwa Dec 7, 2025
453e00a
:memo: Add to CHANGELOG
marcelwa Dec 7, 2025
b81e3e3
:memo: Adjust documentation
marcelwa Dec 7, 2025
2505f27
:pencil2: Fix typo
marcelwa Dec 7, 2025
9ab82f3
:adhesive_bandage: Fix usage of `R` gate in docstring
marcelwa Dec 7, 2025
06a40af
:art: Apply CodeRabbit's suggestion
marcelwa Dec 7, 2025
92ed957
:art: Apply CodeRabbit's suggestion
marcelwa Dec 8, 2025
0911f7c
:white_check_mark: Add tests to cover CodeRabbit's suggestions
marcelwa Dec 8, 2025
3d74d1b
🎨 pre-commit fixes
pre-commit-ci[bot] Dec 9, 2025
9915b6a
Merge branch 'main' into multi_control
keefehuang Dec 9, 2025
cdc0ec3
Merge branch 'main' into multi_control
keefehuang Dec 15, 2025
ee9b279
Rename variables in function definition of addCrz to remove linting w…
Dec 15, 2025
cbda9d6
Remove unreachable branches in switch case in addMcrz
Dec 15, 2025
a8af5a7
Remove unreachable branches in switch cases for
Dec 15, 2025
8236fbf
Use addMcz implementation
Dec 15, 2025
14e1955
Add tests to increase codecov
Dec 15, 2025
9de9ac9
Add more connection assertions for tests
Dec 15, 2025
826829e
Remove one check in buildFunctionality as we only care if there is on…
Dec 15, 2025
93bf49d
Add additional checks per coderabbit suggestions
Jan 8, 2026
48caa99
Merge branch 'main' into multi_control
keefehuang Jan 8, 2026
8cbe448
Fix connectivity in test
Jan 8, 2026
e10767e
Add tests for connectivity of additional wires to meet coderabbit req…
Jan 8, 2026
9816218
Add connectivity test for qubit 1
Jan 8, 2026
ef2f343
Merge branch 'main' into multi_control
keefehuang Jan 8, 2026
77dae77
Removed unneeded import
Jan 8, 2026
6460a92
Apply suggestion from @burgholzer
keefehuang Jan 26, 2026
4af8da7
Remove unneeded import, remove repeated test
Feb 5, 2026
63f2325
Extract checkEquivalence as separate function
Feb 5, 2026
eed43bf
Remove unreachable code
Feb 5, 2026
bf5e23e
Add set_difference implementation
Feb 5, 2026
3f1fa28
Inline vector
Feb 5, 2026
340bad9
Reserve and switch to emplace_back
Feb 5, 2026
4780212
🎨 pre-commit fixes
pre-commit-ci[bot] Feb 5, 2026
ff77fa4
Run clang-format
Feb 5, 2026
297078e
Implement Crx, Mcrz, Mcrzz, Mcrzx, Mcrxx, Mcswap
Feb 5, 2026
9ab3177
🎨 pre-commit fixes
pre-commit-ci[bot] Feb 5, 2026
553ab8f
Update checkEquivalance, tests, Mcrzz implementation
Feb 19, 2026
67b8fc2
🎨 pre-commit fixes
pre-commit-ci[bot] Feb 19, 2026
440ff42
Extend test coverage
Feb 19, 2026
e82defd
🎨 pre-commit fixes
pre-commit-ci[bot] Feb 19, 2026
1f224ce
Apply coderabbit suggestions
Feb 19, 2026
18a8aa1
Update per clang-tidy
Feb 19, 2026
c4c524c
Remove static from checkEquivalence
Feb 19, 2026
4a51d10
Merge branch 'main' into multi_control
keefehuang Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions include/mqt-core/zx/FunctionalityConstruction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ class FunctionalityConstruction {
const std::optional<double>& unconvertedBeta = std::nullopt);
static void addCcx(ZXDiagram& diag, Qubit ctrl0, Qubit ctrl1, Qubit target,
std::vector<Vertex>& qubits);
static void addCcz(ZXDiagram& diag, Qubit ctrl0, Qubit ctrl1, Qubit target,
std::vector<Vertex>& qubits);
static void addCrz(ZXDiagram& diag, const PiExpression& phase,
const Qubit control, const Qubit target,
std::vector<Vertex>& qubits);
static void addMcrz(ZXDiagram& diag, const PiExpression& phase,
std::vector<Qubit> controls, const Qubit target,
std::vector<Vertex>& qubits);
static void addMcx(ZXDiagram& diag, std::vector<Qubit> controls,
const Qubit target, std::vector<Vertex>& qubits);
static void addMcz(ZXDiagram& diag, std::vector<Qubit> controls,
const Qubit target, std::vector<Vertex>& qubits);
static op_it parseOp(ZXDiagram& diag, op_it it, op_it end,
std::vector<Vertex>& qubits, const qc::Permutation& p);
static op_it parseCompoundOp(ZXDiagram& diag, op_it it, op_it end,
Expand Down
185 changes: 183 additions & 2 deletions src/zx/FunctionalityConstruction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,147 @@
addCnot(diag, ctrl0, ctrl1, qubits);
}

void FunctionalityConstruction::addCcz(ZXDiagram& diag, const Qubit ctrl0,
const Qubit ctrl1, const Qubit target,
std::vector<Vertex>& qubits) {

addCnot(diag, ctrl1, target, qubits);
addZSpider(diag, target, qubits, PiExpression(PiRational(-1, 4)));
addCnot(diag, ctrl0, target, qubits);
addZSpider(diag, target, qubits, PiExpression(PiRational(1, 4)));
addCnot(diag, ctrl1, target, qubits);
addZSpider(diag, ctrl1, qubits, PiExpression(PiRational(1, 4)));
addZSpider(diag, target, qubits, PiExpression(PiRational(-1, 4)));
addCnot(diag, ctrl0, target, qubits);
addZSpider(diag, target, qubits, PiExpression(PiRational(1, 4)));
addCnot(diag, ctrl0, ctrl1, qubits);
addZSpider(diag, ctrl0, qubits, PiExpression(PiRational(1, 4)));
addZSpider(diag, ctrl1, qubits, PiExpression(PiRational(-1, 4)));
addZSpider(diag, target, qubits, PiExpression(PiRational(0, 1)),
EdgeType::Hadamard);
addCnot(diag, ctrl0, ctrl1, qubits);
addZSpider(diag, target, qubits, PiExpression(), EdgeType::Hadamard);
}

void FunctionalityConstruction::addCrz(ZXDiagram& diag,
const PiExpression& phase,
const Qubit control, const Qubit target,
std::vector<Vertex>& qubits) {
addCnot(diag, target, control, qubits);

Check warning on line 336 in src/zx/FunctionalityConstruction.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

src/zx/FunctionalityConstruction.cpp:336:3 [readability-suspicious-call-argument]

2nd argument 'target' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'control' (passed to 'target')
addZSpider(diag, control, qubits, -phase / 2);
addZSpider(diag, target, qubits, phase / 2);
addCnot(diag, target, control, qubits);

Check warning on line 339 in src/zx/FunctionalityConstruction.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

src/zx/FunctionalityConstruction.cpp:339:3 [readability-suspicious-call-argument]

2nd argument 'target' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'control' (passed to 'target')
}

void FunctionalityConstruction::addMcrz(ZXDiagram& diag,
const PiExpression& phase,
std::vector<Qubit> controls,
const Qubit target,
std::vector<Vertex>& qubits) {

switch (controls.size()) {
case 0:
addZSpider(diag, target, qubits, phase);
return;
case 1:
addCrz(diag, phase, controls.front(), target, qubits);
return;
default:
const Qubit nextControl = controls.back();
controls.pop_back();

addCrz(diag, phase / 2, nextControl, target, qubits);
addMcx(diag, controls, target, qubits);
addCrz(diag, -phase / 2, nextControl, target, qubits);
addMcx(diag, controls, target, qubits);
}
}

void FunctionalityConstruction::addMcx(ZXDiagram& diag,
std::vector<Qubit> controls,
const Qubit target,
std::vector<Vertex>& qubits) {

switch (controls.size()) {
case 0:
addXSpider(diag, target, qubits, PiExpression(PiRational(1, 1)));
return;
case 1:
addCnot(diag, controls.front(), target, qubits);
return;
case 2:
addCcx(diag, controls.front(), controls.back(), target, qubits);
return;
default:
const auto half = static_cast<std::ptrdiff_t>((controls.size() + 1) / 2);
const std::vector<Qubit> first(controls.begin(), controls.begin() + half);
std::vector<Qubit> second(controls.begin() + half, controls.end());

if (qubits.size() > controls.size() + 1) {
controls.push_back(target);
Qubit anc = -1;
auto it = std::ranges::find_if(qubits, [&](Vertex x) {
return std::ranges::find(controls, static_cast<Qubit>(x)) ==
controls.end();
});
if (it == qubits.end()) {
throw ZXException("No ancilla qubit available for MCX decomposition");
} else {

Check warning on line 395 in src/zx/FunctionalityConstruction.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

src/zx/FunctionalityConstruction.cpp:395:9 [readability-else-after-return]

do not use 'else' after 'throw'
anc = static_cast<Qubit>(*it);
}
controls.pop_back();
second.push_back(anc);

addMcx(diag, first, anc, qubits);
addMcx(diag, second, target, qubits);

addMcx(diag, first, anc, qubits);
addMcx(diag, second, target, qubits);
} else {
addRx(diag, PiExpression(PiRational(1, 4)), target, qubits);
addMcz(diag, second, target, qubits);
addRx(diag, PiExpression(-PiRational(1, 4)), target, qubits);
addMcx(diag, first, target, qubits);

addRx(diag, PiExpression(PiRational(1, 4)), target, qubits);
addMcz(diag, second, target, qubits);
addRx(diag, PiExpression(-PiRational(1, 4)), target, qubits);
addMcx(diag, first, target, qubits);
const Qubit lastControl = controls.back();
controls.pop_back();
addMcrz(diag, PiExpression(PiRational(1, 2)), controls, lastControl,
qubits);
}
}
}
Comment on lines +433 to +474
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

addMcx divide-and-conquer decomposition: second can be const.

Static analysis flags that second (line 451) is never modified after construction.

Proposed fix
-    std::vector<Qubit> second(controls.begin() + half, controls.end());
+    const std::vector<Qubit> second(controls.begin() + half, controls.end());
🧰 Tools
🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint

[warning] 451-451: src/zx/FunctionalityConstruction.cpp:451:5 [misc-const-correctness]
variable 'second' of type 'std::vector' (aka 'vector') can be declared 'const'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zx/FunctionalityConstruction.cpp` around lines 433 - 474, The local
vector second in FunctionalityConstruction::addMcx is never modified after
construction, so change its declaration to a const std::vector<Qubit>
second(controls.begin() + half, controls.end()); to signal immutability and
satisfy static analysis; keep first and controls as-is (note controls is later
mutated via controls.pop_back()) and ensure no other uses assume second is
non-const.


void FunctionalityConstruction::addMcz(ZXDiagram& diag,
std::vector<Qubit> controls,
const Qubit target,
std::vector<Vertex>& qubits) {

switch (controls.size()) {
case 0:
addZSpider(diag, target, qubits, PiExpression(PiRational(1, 1)));
return;
case 1:
addCrz(diag, PiExpression(PiRational(1, 1)), controls.front(), target,
qubits);
return;
case 2:
addCcz(diag, controls.front(), controls.back(), target, qubits);
return;
default:
const Qubit nextControl = controls.back();
controls.pop_back();

addCrz(diag, PiExpression(PiRational(1, 2)), nextControl, target, qubits);
addMcx(diag, controls, target, qubits);
addCrz(diag, PiExpression(-PiRational(1, 2)), nextControl, target, qubits);
addMcx(diag, controls, target, qubits);
}
}
Comment on lines +476 to +483
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Move controls to avoid unnecessary copy.

Same as addMcrxcontrols is taken by value and only forwarded to addMcx.

Proposed fix
 void FunctionalityConstruction::addMcz(ZXDiagram& diag,
                                        std::vector<Qubit> controls,
                                        const Qubit target,
                                        std::vector<Vertex>& qubits) {
   addZSpider(diag, target, qubits, PiExpression(), EdgeType::Hadamard);
-  addMcx(diag, controls, target, qubits);
+  addMcx(diag, std::move(controls), target, qubits);
   addZSpider(diag, target, qubits, PiExpression(), EdgeType::Hadamard);
 }
🧰 Tools
🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint

[warning] 481-481: src/zx/FunctionalityConstruction.cpp:481:16 [performance-unnecessary-value-param]
parameter 'controls' is passed by value and only copied once; consider moving it to avoid unnecessary copies

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zx/FunctionalityConstruction.cpp` around lines 476 - 483, The function
addMcz takes controls by value but forwards it to addMcx without moving, causing
an extra copy; update the call to addMcx to forward ownership by calling
addMcx(diag, std::move(controls), target, qubits) (or change the parameter to an
rvalue reference and move accordingly) while keeping the rest of addMcz the
same; reference symbols: addMcz, addMcx, addMcrx.


FunctionalityConstruction::op_it
FunctionalityConstruction::parseOp(ZXDiagram& diag, op_it it, op_it end,
std::vector<Vertex>& qubits,
Expand Down Expand Up @@ -538,6 +679,9 @@
qubits[static_cast<std::size_t>(target)],
EdgeType::Hadamard);
break;
case qc::OpType::RZ:
addCrz(diag, parseParam(op.get(), 0), ctrl, target, qubits);
break;

case qc::OpType::I:
break;
Expand Down Expand Up @@ -578,16 +722,42 @@
ctrl1 = static_cast<Qubit>(p.at(ctrl.qubit));
}
}
std::vector<Qubit> controls;
for (const auto& ctrl : op->getControls()) {
controls.push_back(static_cast<Qubit>(p.at(ctrl.qubit)));
}
switch (op->getType()) {
case qc::OpType::X:
addCcx(diag, ctrl0, ctrl1, target, qubits);
break;

case qc::OpType::Z:
addCcz(diag, ctrl0, ctrl1, target, qubits);
break;
case qc::OpType::RZ:
addMcrz(diag, parseParam(op.get(), 0), controls, target, qubits);
break;
default:
throw ZXException("Unsupported Multi-control operation: " +
qc::toString(op->getType()));
}
} else if (op->getNtargets() == 1) {
const auto target = static_cast<Qubit>(p.at(op->getTargets().front()));
std::vector<Qubit> controls;
for (const auto& ctrl : op->getControls()) {
controls.push_back(static_cast<Qubit>(p.at(ctrl.qubit)));
}
switch (op->getType()) {
case qc::OpType::X:
addMcx(diag, controls, target, qubits);
break;
case qc::OpType::Z:
addZSpider(diag, target, qubits, PiExpression(), EdgeType::Hadamard);
addCcx(diag, ctrl0, ctrl1, target, qubits);
addMcx(diag, controls, target, qubits);
addZSpider(diag, target, qubits, PiExpression(), EdgeType::Hadamard);
break;
case qc::OpType::RZ:
addMcrz(diag, parseParam(op.get(), 0), controls, target, qubits);
break;
default:
throw ZXException("Unsupported Multi-control operation: " +
qc::toString(op->getType()));
Expand Down Expand Up @@ -701,15 +871,26 @@
case qc::OpType::S:
case qc::OpType::Tdg:
case qc::OpType::Sdg:
case qc::OpType::RZ:
return true;

default:
return false;
}
} else if (op->getNcontrols() == 2) {

Check warning on line 880 in src/zx/FunctionalityConstruction.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

src/zx/FunctionalityConstruction.cpp:880:39 [bugprone-branch-clone]

repeated branch body in conditional chain
switch (op->getType()) {
case qc::OpType::X:
case qc::OpType::Z:
case qc::OpType::RZ:
return true;
default:
return false;
}
} else if (op->getNtargets() == 1) {
switch (op->getType()) {
case qc::OpType::X:
case qc::OpType::Z:
case qc::OpType::RZ:
return true;
default:
return false;
Expand Down
139 changes: 134 additions & 5 deletions test/zx/test_zx_functionality.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,143 @@ TEST_F(ZXFunctionalityTest, Compound) {
EXPECT_TRUE(diag.isIdentity());
}

TEST_F(ZXFunctionalityTest, UnsupportedMultiControl) {
// TEST_F(ZXFunctionalityTest, UnsupportedMultiControl) {
// using namespace qc::literals;
// qc = qc::QuantumComputation(4);
// qc.mcx({1, 2, 3}, 0);
// EXPECT_FALSE(FunctionalityConstruction::transformableToZX(&qc));
// EXPECT_THROW(const ZXDiagram diag =
// FunctionalityConstruction::buildFunctionality(&qc),
// ZXException);
// }

TEST_F(ZXFunctionalityTest, CRZ) {
using namespace qc::literals;
qc = qc::QuantumComputation(2);
qc.crz(PI / 2, 0, 1);

auto qcPrime = qc::QuantumComputation(2);

qcPrime.cx(0, 1);
qcPrime.rz(-PI / 4, 1);
qcPrime.cx(0, 1);
qcPrime.rz(PI / 4, 1);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

auto dPrime = FunctionalityConstruction::buildFunctionality(&qcPrime);

d.concat(dPrime.invert());

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, MultiControlX) {
using namespace qc::literals;
qc = qc::QuantumComputation(4);
qc.mcx({1, 2, 3}, 0);
EXPECT_FALSE(FunctionalityConstruction::transformableToZX(&qc));
EXPECT_THROW(const ZXDiagram diag =
FunctionalityConstruction::buildFunctionality(&qc),
ZXException);

auto qcPrime = qc::QuantumComputation(4);
qcPrime.mcx({1, 2, 3}, 0);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

auto dPrime = FunctionalityConstruction::buildFunctionality(&qcPrime);

d.concat(dPrime.invert());

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, MultiControlXLarger) {
using namespace qc::literals;
qc = qc::QuantumComputation(5);
qc.mcx({1, 2, 3}, 0);

auto qcPrime = qc::QuantumComputation(5);
qcPrime.mcx({1, 2}, 4);
qcPrime.mcx({3, 4}, 0);
qcPrime.mcx({1, 2}, 4);
qcPrime.mcx({3, 4}, 0);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

auto dPrime = FunctionalityConstruction::buildFunctionality(&qcPrime);

d.concat(dPrime.invert());

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, MultiControlX0) {
using namespace qc::literals;
qc = qc::QuantumComputation(1);
qc.mcx({}, 0);

auto qcPrime = qc::QuantumComputation(1);

qcPrime.x(0);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

auto dPrime = FunctionalityConstruction::buildFunctionality(&qcPrime);

d.concat(dPrime.invert());

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, MultiControlX1) {
using namespace qc::literals;
qc = qc::QuantumComputation(2);
qc.mcx({1}, 0);

auto qcPrime = qc::QuantumComputation(2);

qcPrime.cx(1, 0);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

auto dPrime = FunctionalityConstruction::buildFunctionality(&qcPrime);

d.concat(dPrime.invert());

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, MultiCRZ) {
using namespace qc::literals;
qc = qc::QuantumComputation(3);
qc.mcrz(PI / 4, {1, 2}, 0);
qc.mcrz(-PI / 4, {1, 2}, 0);

auto d = FunctionalityConstruction::buildFunctionality(&qc);

fullReduce(d);

EXPECT_TRUE(d.isIdentity());
EXPECT_TRUE(d.globalPhaseIsZero());
EXPECT_TRUE(d.connected(d.getInput(0), d.getOutput(0)));
}

TEST_F(ZXFunctionalityTest, UnsupportedControl) {
Expand Down
Loading