Skip to content

Commit 47730e7

Browse files
authored
🚸 Allow indexed registers as operation arguments (#839)
## Description This update enables the use of indexed registers as arguments for operations. For example, in Python: ```python from mqt.core.ir import QuantumComputation qc = QuantumComputation() q = qc.add_qubit_register(2) c = qc.add_classical_register(2) qc.h(q[0]) qc.cx(q[0], q[1]) qc.measure(q[0], c[0]) qc.measure(q[1], c[1]) ``` In C++: ```c++ #include "ir/QuantumComputation.hpp" auto qc = qc::QuantumComputation(); const auto& q = qc.addQubitRegister(2); const auto& c = qc.addClassicalRegister(2); qc.h(q[0]); qc.cx(q[0], q[1]); qc.measure(q[0], c[0]); qc.measure(q[1], c[1]); ``` This improvement expands the usability of the IR as it no longer requires users to track the order in which registers were added to infer their qubits' indices. ## 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.
1 parent ec8a6f1 commit 47730e7

File tree

9 files changed

+124
-22
lines changed

9 files changed

+124
-22
lines changed

docs/mqt_core_ir.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,35 @@ precision = 3
4747
qc = QuantumComputation()
4848
4949
# Counting register
50-
qc.add_qubit_register(1, "q")
50+
q = qc.add_qubit_register(1, "q")
5151
5252
# Eigenstate register
53-
qc.add_qubit_register(1, "psi")
53+
psi = qc.add_qubit_register(1, "psi")
5454
5555
# Classical register for the result, the estimated phase is `0.c_2 c_1 c_0 * pi`
56-
qc.add_classical_register(precision, "c")
56+
c = qc.add_classical_register(precision, "c")
5757
5858
# Prepare psi in the eigenstate |1>
59-
qc.x(1)
59+
qc.x(psi[0])
6060
6161
for i in range(precision):
6262
# Hadamard on the working qubit
63-
qc.h(0)
63+
qc.h(q[0])
6464
6565
# Controlled phase gate
66-
qc.cp(2**(precision - i - 1) * theta, 0, 1)
66+
qc.cp(2**(precision - i - 1) * theta, q[0], psi[0])
6767
6868
# Iterative inverse QFT
6969
for j in range(i):
70-
qc.classic_controlled(op="p", target=0, cbit=j, params=[-pi / 2**(i - j)])
71-
qc.h(0)
70+
qc.classic_controlled(op="p", target=q[0], cbit=c[j], params=[-pi / 2**(i - j)])
71+
qc.h(q[0])
7272
7373
# Measure the result
74-
qc.measure(0, i)
74+
qc.measure(q[0], c[i])
7575
7676
# Reset the qubit if not finished
7777
if i < precision - 1:
78-
qc.reset(0)
78+
qc.reset(q[0])
7979
```
8080

8181
The circuit class provides lots of flexibility when it comes to the kind of gates that can be applied.

include/mqt-core/ir/QuantumComputation.hpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,16 @@ class QuantumComputation {
371371
const std::string& registerName = "c");
372372

373373
// this function augments a given circuit by additional registers
374-
void addQubitRegister(std::size_t nq, const std::string& regName = "q");
374+
const QuantumRegister& addQubitRegister(std::size_t nq,
375+
const std::string& regName = "q");
375376
const ClassicalRegister&
376377
addClassicalRegister(std::size_t nc, const std::string& regName = "c");
377-
void addAncillaryRegister(std::size_t nq, const std::string& regName = "anc");
378+
const QuantumRegister&
379+
addAncillaryRegister(std::size_t nq, const std::string& regName = "anc");
378380
// a function to combine all quantum registers (qregs and ancregs) into a
379381
// single register (useful for circuits mapped to a device)
380-
void unifyQuantumRegisters(const std::string& regName = "q");
382+
const QuantumRegister&
383+
unifyQuantumRegisters(const std::string& regName = "q");
381384

382385
/**
383386
* @brief Removes a logical qubit

include/mqt-core/ir/Register.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,21 @@ template <typename BitType> class Register {
5454
return globalIndex - startIndex;
5555
}
5656

57+
[[nodiscard]] BitType getGlobalIndex(const BitType localIndex) const {
58+
if (localIndex >= size) {
59+
throw std::out_of_range("Index out of range");
60+
}
61+
return startIndex + localIndex;
62+
}
63+
5764
[[nodiscard]] std::string toString(const BitType globalIndex) const {
5865
return name + "[" + std::to_string(getLocalIndex(globalIndex)) + "]";
5966
}
6067

68+
[[nodiscard]] BitType operator[](const BitType localIndex) const {
69+
return getGlobalIndex(localIndex);
70+
}
71+
6172
private:
6273
BitType startIndex;
6374
std::size_t size;

src/ir/QuantumComputation.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,9 @@ void QuantumComputation::initializeIOMapping() {
344344
}
345345
}
346346

347-
void QuantumComputation::addQubitRegister(std::size_t nq,
348-
const std::string& regName) {
347+
const QuantumRegister&
348+
QuantumComputation::addQubitRegister(std::size_t nq,
349+
const std::string& regName) {
349350
if (quantumRegisters.count(regName) != 0) {
350351
throw QFRException("[addQubitRegister] Register " + regName +
351352
" already exists");
@@ -372,6 +373,7 @@ void QuantumComputation::addQubitRegister(std::size_t nq,
372373
nqubits += nq;
373374
ancillary.resize(nqubits + nancillae);
374375
garbage.resize(nqubits + nancillae);
376+
return quantumRegisters.at(regName);
375377
}
376378

377379
const ClassicalRegister&
@@ -393,8 +395,9 @@ QuantumComputation::addClassicalRegister(std::size_t nc,
393395
return it->second;
394396
}
395397

396-
void QuantumComputation::addAncillaryRegister(std::size_t nq,
397-
const std::string& regName) {
398+
const QuantumRegister&
399+
QuantumComputation::addAncillaryRegister(std::size_t nq,
400+
const std::string& regName) {
398401
if (ancillaRegisters.count(regName) != 0) {
399402
throw QFRException("[addAncillaryRegister] Register " + regName +
400403
" already exists");
@@ -416,6 +419,7 @@ void QuantumComputation::addAncillaryRegister(std::size_t nq,
416419
ancillary[j] = true;
417420
}
418421
nancillae += nq;
422+
return ancillaRegisters.at(regName);
419423
}
420424

421425
std::pair<Qubit, std::optional<Qubit>>
@@ -938,12 +942,14 @@ bool QuantumComputation::isLastOperationOnQubit(
938942
return true;
939943
}
940944

941-
void QuantumComputation::unifyQuantumRegisters(const std::string& regName) {
945+
const QuantumRegister&
946+
QuantumComputation::unifyQuantumRegisters(const std::string& regName) {
942947
ancillaRegisters.clear();
943948
quantumRegisters.clear();
944949
nqubits += nancillae;
945950
nancillae = 0;
946951
quantumRegisters.try_emplace(regName, 0, nqubits, regName);
952+
return quantumRegisters.at(regName);
947953
}
948954

949955
void QuantumComputation::appendMeasurementsAccordingToOutputPermutation(

src/mqt/core/ir/__init__.pyi

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ from os import PathLike
1212
from typing import overload
1313

1414
from .operations import ComparisonKind, Control, Operation, OpType
15-
from .registers import ClassicalRegister
15+
from .registers import ClassicalRegister, QuantumRegister
1616
from .symbolic import Expression, Variable
1717

1818
__all__ = [
@@ -307,12 +307,15 @@ class QuantumComputation(MutableSequence[Operation]):
307307
# (Qu)Bit Registers
308308
# --------------------------------------------------------------------------
309309

310-
def add_ancillary_register(self, n: int, name: str = "anc") -> None:
310+
def add_ancillary_register(self, n: int, name: str = "anc") -> QuantumRegister:
311311
"""Add an ancillary register to the quantum computation.
312312
313313
Args:
314314
n: The number of qubits in the ancillary register.
315315
name: The name of the ancillary register.
316+
317+
Returns:
318+
The ancillary register added to the quantum computation.
316319
"""
317320

318321
def add_classical_register(self, n: int, name: str = "c") -> ClassicalRegister:
@@ -326,19 +329,25 @@ class QuantumComputation(MutableSequence[Operation]):
326329
The classical register added to the quantum computation.
327330
"""
328331

329-
def add_qubit_register(self, n: int, name: str = "q") -> None:
332+
def add_qubit_register(self, n: int, name: str = "q") -> QuantumRegister:
330333
"""Add a qubit register to the quantum computation.
331334
332335
Args:
333336
n: The number of qubits in the qubit register.
334337
name: The name of the qubit register.
338+
339+
Returns:
340+
The qubit register added to the quantum computation.
335341
"""
336342

337-
def unify_quantum_registers(self, name: str = "q") -> None:
343+
def unify_quantum_registers(self, name: str = "q") -> QuantumRegister:
338344
"""Unify all quantum registers in the quantum computation.
339345
340346
Args:
341347
name: The name of the unified quantum register.
348+
349+
Returns:
350+
The unified quantum register.
342351
"""
343352

344353
# --------------------------------------------------------------------------

src/mqt/core/ir/registers.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class QuantumRegister:
4242
def __hash__(self) -> int:
4343
"""Return the hash of the quantum register."""
4444

45+
def __getitem__(self, key: int) -> int:
46+
"""Get the qubit at the specified index."""
47+
4548
def __contains__(self, qubit: int) -> bool:
4649
"""Check if the quantum register contains a qubit."""
4750

@@ -80,5 +83,8 @@ class ClassicalRegister:
8083
def __hash__(self) -> int:
8184
"""Return the hash of the classical register."""
8285

86+
def __getitem__(self, key: int) -> int:
87+
"""Get the bit at the specified index."""
88+
8389
def __contains__(self, bit: int) -> bool:
8490
"""Check if the classical register contains a bit."""

src/python/ir/register_registers.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
#include "ir/Register.hpp"
1212
#include "python/pybind11.hpp"
1313

14+
#include <cstddef>
1415
#include <pybind11/operators.h>
16+
#include <string>
1517

1618
namespace mqt {
1719

@@ -35,6 +37,7 @@ void registerRegisters(py::module& m) {
3537
.def(py::self == py::self)
3638
.def(py::self != py::self)
3739
.def(hash(py::self))
40+
.def("__getitem__", &qc::QuantumRegister::getGlobalIndex, "key"_a)
3841
.def("__contains__", &qc::QuantumRegister::contains)
3942
.def("__repr__", [](const qc::QuantumRegister& reg) {
4043
return "QuantumRegister(name=" + reg.getName() +
@@ -62,6 +65,7 @@ void registerRegisters(py::module& m) {
6265
.def(py::self == py::self)
6366
.def(py::self != py::self)
6467
.def(hash(py::self))
68+
.def("__getitem__", &qc::ClassicalRegister::getGlobalIndex, "key"_a)
6569
.def("__contains__", &qc::ClassicalRegister::contains)
6670
.def("__repr__", [](const qc::ClassicalRegister& reg) {
6771
return "ClassicalRegister(name=" + reg.getName() +

test/ir/test_io.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,3 +661,26 @@ TEST_F(IO, dumpingIncompleteOutputPermutationNotStartingAtZero) {
661661
const auto qc2 = qasm3::Importer::imports(qasm);
662662
EXPECT_EQ(qc, qc2);
663663
}
664+
665+
TEST_F(IO, indexedRegisterOperands) {
666+
const auto& q = qc.addQubitRegister(2);
667+
const auto& c = qc.addClassicalRegister(2);
668+
669+
qc.h(q[0]);
670+
qc.cx(q[0], q[1]);
671+
qc.measure(q[0], c[0]);
672+
qc.measure(q[1], c[1]);
673+
674+
const auto qasm = qc.toQASM();
675+
const auto* const expected = "// i 0 1\n"
676+
"// o 0 1\n"
677+
"OPENQASM 3.0;\n"
678+
"include \"stdgates.inc\";\n"
679+
"qubit[2] q;\n"
680+
"bit[2] c;\n"
681+
"h q[0];\n"
682+
"cx q[0], q[1];\n"
683+
"c[0] = measure q[0];\n"
684+
"c[1] = measure q[1];\n";
685+
EXPECT_EQ(qasm, expected);
686+
}

test/python/ir/test_ir.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) 2025 Chair for Design Automation, TUM
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: MIT
5+
#
6+
# Licensed under the MIT License
7+
8+
"""Test the quantum computation IR."""
9+
10+
from __future__ import annotations
11+
12+
from mqt.core.ir import QuantumComputation
13+
14+
15+
def test_bell_state_circuit() -> None:
16+
"""Test the creation of a Bell state circuit."""
17+
qc = QuantumComputation()
18+
q = qc.add_qubit_register(2)
19+
c = qc.add_classical_register(2)
20+
21+
qc.h(q[0])
22+
qc.cx(q[0], q[1])
23+
qc.measure(q[0], c[0])
24+
qc.measure(q[1], c[1])
25+
26+
qasm = qc.qasm3_str()
27+
expected = """
28+
// i 0 1
29+
// o 0 1
30+
OPENQASM 3.0;
31+
include "stdgates.inc";
32+
qubit[2] q;
33+
bit[2] c;
34+
h q[0];
35+
cx q[0], q[1];
36+
c[0] = measure q[0];
37+
c[1] = measure q[1];
38+
"""
39+
# Remove all whitespace from both strings before comparison
40+
assert "".join(qasm.split()) == "".join(expected.split())

0 commit comments

Comments
 (0)