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 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/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; } }, 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)