From 6767dd2c61b148b44c2d1e86de9b993cae154c39 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sat, 5 Sep 2026 09:50:49 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Initialize=20Qiskit=20classi?= =?UTF-8?q?cal=20bits=20in=20QASM=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve Qiskit zero-initialization semantics when a circuit measures only part of its classical storage. Assisted-by: GPT-5 via Codex --- python/mqt/core/plugins/qiskit/backend.py | 13 +++++++++++++ .../plugins/qiskit/test_mock_backend.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index 940f8128f5..ccdc01b5a9 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -122,6 +122,19 @@ def _serialize_to_qasm3(circuit: QuantumCircuit, backend: QDMIBackend) -> str: Returns: The OpenQASM 3 program. """ + # Qiskit classical bits start at zero, while OpenQASM 3 bits are + # uninitialized. Preserve Qiskit's semantics and make every output valid + # even when the circuit measures only part of a register. + if circuit.num_clbits: + initialization = circuit.copy_empty_like(vars_mode="drop") + initialization.global_phase = 0 + for clbit in initialization.clbits: + initialization.store( + clbit, + False, # ruff: ignore[boolean-positional-value-in-call] Qiskit store arguments are positional-only. + ) + circuit = circuit.compose(initialization, front=True, inplace=False) + # Qiskit's OpenQASM3 exporter is fairly limited in terms of which gates it supports natively. # So it needs some help from us. exclusion_list = set() diff --git a/test/python/plugins/qiskit/test_mock_backend.py b/test/python/plugins/qiskit/test_mock_backend.py index 9a246707e3..7124a16cf8 100644 --- a/test/python/plugins/qiskit/test_mock_backend.py +++ b/test/python/plugins/qiskit/test_mock_backend.py @@ -492,6 +492,25 @@ def test_backend_qasm3_serialization_success(mock_qdmi_device_factory: type[Mock assert "cx q[0], q[1]" in program +def test_backend_qasm3_zero_initializes_classical_bits( + mock_qdmi_device_factory: type[MockQDMIDevice], +) -> None: + """Initialize every QASM 3 classical bit before measurement.""" + qc = QuantumCircuit(2, 2) + qc.measure(0, 0) + + device = mock_qdmi_device_factory(num_qubits=2, operations=["measure"]) + backend = QDMIBackend(device) # ty: ignore[invalid-argument-type] + + program, fmt = backend._serialize_circuit(qc, [ProgramFormat.QASM3]) # ruff:ignore[private-member-access] + + assert fmt == ProgramFormat.QASM3 + assert isinstance(program, str) + assert "c[0] = false;" in program + assert "c[1] = false;" in program + assert program.index("c[0] = false;") < program.index("c[0] = measure q[0];") + + def test_backend_qasm2_serialization_success(mock_qdmi_device_factory: type[MockQDMIDevice]) -> None: """Backend should successfully serialize a circuit into OpenQASM 2.""" qc = QuantumCircuit(2) From 97cfad19aa0f7954699140a26b4d9b05979c41cf Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sat, 5 Sep 2026 09:52:15 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20Document=20Qiskit=20classica?= =?UTF-8?q?l=20initialization=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5 via Codex --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1dbe1f4f..15e46ecaeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,9 @@ releases may include breaking changes. ### Fixed +- 🐛 Initialize Qiskit classical bits before OpenQASM 3 serialization so + partially measured circuits preserve their zero values ([#2399]) + ([**@burgholzer**]) - 🐛 Handle empty DDSIM results and NUL-terminated QDMI result buffers ([#2288]) ([**@simon1hofmann**]) - 🐛 Validate output permutations before I/O mapping initialization ([#2278]) @@ -816,6 +819,7 @@ for previous changelogs._ +[#2399]: https://github.com/munich-quantum-toolkit/core/pull/2399 [#2368]: https://github.com/munich-quantum-toolkit/core/pull/2368 [#2358]: https://github.com/munich-quantum-toolkit/core/pull/2358 [#2349]: https://github.com/munich-quantum-toolkit/core/pull/2349 From 27df5426b68dfeba4d63d3fb2372b22917409b33 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sat, 5 Sep 2026 10:34:44 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Accept=20leading=20QASM=203?= =?UTF-8?q?=20bit=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teach the v3 importer to recognize indexed bit targets and treat leading false stores as the legacy IR default, while continuing to reject later classical computation. Assisted-by: GPT-5 via Codex --- src/qasm3/Importer.cpp | 10 ++++++++++ src/qasm3/passes/TypeCheckPass.cpp | 14 ++++++++++++-- test/ir/test_qasm3_parser.cpp | 28 +++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/qasm3/Importer.cpp b/src/qasm3/Importer.cpp index ab8b5ab291..ed260a7595 100644 --- a/src/qasm3/Importer.cpp +++ b/src/qasm3/Importer.cpp @@ -414,6 +414,16 @@ void Importer::visitAssignmentStatement( return; } + if (const auto constant = std::dynamic_pointer_cast( + assignmentStatement->expression->expression); + assignmentStatement->type == AssignmentStatement::Assignment && + constant && constant->isBool() && !constant->getBool() && qc->empty()) { + std::vector bits; + translateBitOperand(assignmentStatement->identifier, bits, + assignmentStatement->debugInfo); + return; + } + // In the future, handle classical computation. throw CompilerError("Classical computation not supported.", assignmentStatement->debugInfo); diff --git a/src/qasm3/passes/TypeCheckPass.cpp b/src/qasm3/passes/TypeCheckPass.cpp index 79b4acb323..69100f717e 100644 --- a/src/qasm3/passes/TypeCheckPass.cpp +++ b/src/qasm3/passes/TypeCheckPass.cpp @@ -175,10 +175,20 @@ void TypeCheckPass::visitAssignmentStatement( return; } - if (!idTy->second.type->fits(*exprTy.type)) { + auto targetTy = idTy->second.type; + if (!assignmentStatement->identifier->indices.empty()) { + if (const auto designatedTy = + std::dynamic_pointer_cast>(targetTy)) { + targetTy = + std::make_shared>(designatedTy->type, 1); + } + } + + if (!targetTy->fits(*exprTy.type) && + !(targetTy->isConvertibleToBool() && exprTy.type->isBool())) { std::stringstream ss; ss << "Type mismatch in assignment. Expected '"; - ss << idTy->second.type->toString(); + ss << targetTy->toString(); ss << "', found '"; ss << exprTy.type->toString(); ss << "'."; diff --git a/test/ir/test_qasm3_parser.cpp b/test/ir/test_qasm3_parser.cpp index 47ae333509..a216b32aee 100644 --- a/test/ir/test_qasm3_parser.cpp +++ b/test/ir/test_qasm3_parser.cpp @@ -76,6 +76,32 @@ TEST_F(Qasm3ParserTest, ImportQasm3MeasureSingleQubit) { EXPECT_EQ(qc.front()->getType(), qc::Measure); } +TEST_F(Qasm3ParserTest, ImportQasm3ClassicalBitInitialization) { + const std::string testfile = "OPENQASM 3.0;\n" + "qubit[2] q;\n" + "bit[2] c;\n" + "c[0] = false;\n" + "c[1] = false;\n" + "c[0] = measure q[0];"; + const auto qc = qasm3::Importer::imports(testfile); + + EXPECT_EQ(qc.getNcbits(), 2); + ASSERT_EQ(qc.getNindividualOps(), 1); + EXPECT_EQ(qc.front()->getType(), qc::Measure); +} + +TEST_F(Qasm3ParserTest, ImportQasm3RejectsClassicalBitReset) { + const std::string testfile = "OPENQASM 3.0;\n" + "qubit q;\n" + "bit c;\n" + "c = measure q;\n" + "c = false;"; + + EXPECT_THROW( + { const auto qc = qasm3::Importer::imports(testfile); }, + qasm3::CompilerError); +} + TEST_F(Qasm3ParserTest, ImportQasm3RejectsIndexingSingleQubit) { const std::string testfile = "OPENQASM 3.0;\n" "include \"stdgates.inc\";\n" @@ -1783,7 +1809,7 @@ TEST_F(Qasm3ParserTest, ImportQasmAssignmentIndexType) { const auto qc = qasm3::Importer::imports(testfile); } catch (const qasm3::CompilerError& e) { EXPECT_EQ(e.message, "Type Check Error: Type mismatch in assignment. " - "Expected 'bit[16]', found 'uint[32]'."); + "Expected 'bit[1]', found 'uint[32]'."); throw; } },