diff --git a/.gitignore b/.gitignore index d664737b1..d4fe75c91 100644 --- a/.gitignore +++ b/.gitignore @@ -301,3 +301,6 @@ pyrightconfig.json # setuptools_scm src/**/_version.py +.idea/ +# Test Output +tests/circuit_drawings/* diff --git a/pyproject.toml b/pyproject.toml index d654262aa..c2b94cb3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ dependencies = [ "numpy>=2.3.2; python_version >= '3.14'", "scikit-learn>=1.5.2", "scikit-learn>=1.7.2; python_version >= '3.14'", + "mqt-qcec>=3.6.1", + "qiskit-aer>=0.17.2", ] classifiers = [ @@ -81,6 +83,10 @@ dev = [ "prek>=0.2.27", "ty==0.0.40", ] +pytest = [ + "mqt-qcec>=3.6.1", + "qiskit-aer>=0.17.2", +] [project.scripts] "create_mqt_bench_zip" = "mqt.bench.utils:create_zip_file" @@ -250,6 +256,7 @@ aer = "aer" fom = "fom" bench = "bench" benchs = "benchs" +ket = "ket" [tool.repo-review.ignore] diff --git a/src/mqt/bench/benchmark_generation.py b/src/mqt/bench/benchmark_generation.py index 0014ca05c..0b7b4349c 100644 --- a/src/mqt/bench/benchmark_generation.py +++ b/src/mqt/bench/benchmark_generation.py @@ -22,6 +22,9 @@ from qiskit.transpiler import Layout, Target from typing_extensions import assert_never +from .error_correction.shor_transpiler import ShorTranspiler +from .error_correction.steane_transpiler import SteaneTranspiler + if sys.version_info >= (3, 11): from typing import Unpack else: @@ -198,6 +201,7 @@ def get_benchmark_alg( def get_benchmark_alg( benchmark: str | QuantumCircuit, circuit_size: int | None = None, + encoding: str = "", *, generate_mirror_circuit: bool = False, random_parameters: bool = True, @@ -208,6 +212,7 @@ def get_benchmark_alg( Arguments: benchmark: QuantumCircuit or name of the benchmark to be generated circuit_size: Input for the benchmark creation, in most cases this is equal to the qubit number + encoding: Error correction code to be used (currently unused). generate_mirror_circuit: If True, generates the mirror version (U @ U.inverse()) of the benchmark. random_parameters: If True, assigns random parameters to the circuit's parameters if they exist. kwargs: Additional keyword arguments passed to the circuit creation. @@ -216,8 +221,19 @@ def get_benchmark_alg( Qiskit::QuantumCircuit representing the raw benchmark circuit without any hardware-specific compilation or mapping. """ qc = _get_circuit(benchmark, circuit_size, random_parameters, **kwargs) + # Todo: Make it combined with error code if generate_mirror_circuit: return _create_mirror_circuit(qc, inplace=True) + + if encoding == "shor": + transpiler = ShorTranspiler(qc, add_syndromes=True) + transpiler.transpile() + return transpiler.transpiled_qc + if encoding == "steane": + transpiler = SteaneTranspiler(qc, add_syndromes=True) + transpiler.transpile() + return transpiler.transpiled_qc + return qc @@ -478,6 +494,7 @@ def get_benchmark( circuit_size: int, target: Target | None = None, opt_level: int = 2, + encoding: str = "", *, generate_mirror_circuit: bool = False, random_parameters: bool = True, @@ -492,6 +509,7 @@ def get_benchmark( circuit_size: None, target: Target | None = None, opt_level: int = 2, + encoding: str = "", *, generate_mirror_circuit: bool = False, random_parameters: bool = True, @@ -506,6 +524,7 @@ def get_benchmark( circuit_size: int | None = None, target: Target | None = None, opt_level: int = 2, + encoding: str = "", *, generate_mirror_circuit: bool = False, random_parameters: bool = True, @@ -519,6 +538,7 @@ def get_benchmark( circuit_size: int | None = None, target: Target | None = None, opt_level: int = 2, + encoding: str = "", *, generate_mirror_circuit: bool = False, random_parameters: bool = True, @@ -533,6 +553,7 @@ def get_benchmark( target: `~qiskit.transpiler.target.Target` for the benchmark generation (only used for "nativegates" and "mapped" level) opt_level: Optimization level to be used by the transpiler. + encoding: Error correction code to be used (currently unused). generate_mirror_circuit: If True, generates the mirror version (U @ U.inverse()) of the benchmark. random_parameters: If True, assigns random parameters to the circuit's parameters if they exist. kwargs: Additional keyword arguments passed to the circuit creation. @@ -546,6 +567,7 @@ def get_benchmark( circuit_size=circuit_size, generate_mirror_circuit=generate_mirror_circuit, random_parameters=random_parameters, + encoding=encoding, **kwargs, ) diff --git a/src/mqt/bench/benchmarks/seven_qubit_steane_code.py b/src/mqt/bench/benchmarks/seven_qubit_steane_code.py index c355774e9..605eea29d 100644 --- a/src/mqt/bench/benchmarks/seven_qubit_steane_code.py +++ b/src/mqt/bench/benchmarks/seven_qubit_steane_code.py @@ -13,127 +13,14 @@ from qiskit import ClassicalRegister from qiskit.circuit import AncillaRegister, QuantumCircuit, QuantumRegister -from ._registry import register_benchmark - - -def _get_seven_qubit_steane_code_encoding_circuit() -> QuantumCircuit: - """Create the 7-qubit Steane code encoding circuit. - - Encodes qubit 0 into the 7-qubit Steane code logical state: - - |0> -> (|0000000> + |1010101> + |0110011> + |1100110> + |0001111> + |1011010> + |0111100> + |1101001>) - - |1> -> (|1111111> + |0101010> + |1001100> + |0011001> + |1110000> + |0100101> + |1000011> + |0010110>). - - Returns: - QuantumCircuit: 7-qubit encoding circuit. - """ - out = QuantumCircuit(7) - # H - out.h(4) - out.h(5) - out.h(6) - # CNOT from 0 - out.cx(0, 1) - out.cx(0, 2) - # CNOT from 6 - out.cx(6, 3) - out.cx(6, 1) - out.cx(6, 0) - # CNOT from 5 - out.cx(5, 3) - out.cx(5, 2) - out.cx(5, 0) - # CNOT from 4 - out.cx(4, 3) - out.cx(4, 2) - out.cx(4, 1) - return out - - -def _get_seven_qubit_steane_code_decoding_circuit() -> QuantumCircuit: - """Create the 7-qubit Steane code decoding circuit. - - Reverses the encoding operation to extract the logical qubit back to qubit 0. - - Returns: - QuantumCircuit: 7-qubit decoding circuit (qubit 0 is the output qubit). - """ - return _get_seven_qubit_steane_code_encoding_circuit().inverse() - +from mqt.bench.components.steane_circuit_components import ( + apply_seven_qubit_steane_code_correction, + get_seven_qubit_steane_code_decoding_circuit, + get_seven_qubit_steane_code_encoding_circuit, + get_seven_qubit_steane_code_syndrome_extraction_circuit, +) -def _get_seven_qubit_steane_code_syndrome_extraction_circuit() -> QuantumCircuit: - """Create the syndrome extraction circuit for the 7-qubit Steane code. - - Extracts bit-flip and phase-flip syndromes using 6 ancilla qubits (3 for each type). - - Bit-flip syndrome extraction: - Syndrome bits measure the parity of specific qubit subsets corresponding to - the X-stabilizer generators. - - Phase-flip syndrome extraction: - Uses Hadamard gates to convert from Z to X basis, and control/target swapped - CNOTs to extract the phase-flip syndrome - - Syndrome mapping: The 3-bit syndrome value (1-7) directly identifies which - data qubit experienced an error. Syndrome 0 indicates no error. - - Returns: - QuantumCircuit: 13-qubit circuit (qubits 0-6 are data, 7-9 are bit-flip - syndrome ancillas, 10-12 are phase-flip syndrome ancillas). - """ - logical_qubit, bit_flip_syndrome, phase_flip_syndrome = QuantumRegister(7), AncillaRegister(3), AncillaRegister(3) - out = QuantumCircuit(logical_qubit, bit_flip_syndrome, phase_flip_syndrome) - # Bit-flip - for ctrl in (0, 2, 4, 6): - out.cx(logical_qubit[ctrl], bit_flip_syndrome[0]) - for ctrl in (1, 2, 5, 6): - out.cx(logical_qubit[ctrl], bit_flip_syndrome[1]) - for ctrl in (3, 4, 5, 6): - out.cx(logical_qubit[ctrl], bit_flip_syndrome[2]) - # Phase-flip - for i in range(3): - out.h(phase_flip_syndrome[i]) - for targ in (0, 2, 4, 6): - out.cx(phase_flip_syndrome[0], logical_qubit[targ]) - for targ in (1, 2, 5, 6): - out.cx(phase_flip_syndrome[1], logical_qubit[targ]) - for targ in (3, 4, 5, 6): - out.cx(phase_flip_syndrome[2], logical_qubit[targ]) - for i in range(3): - out.h(phase_flip_syndrome[i]) - return out - - -def _apply_seven_qubit_steane_code_correction( - qc: QuantumCircuit, - logical_qubit: QuantumRegister, - bit_flip_syndrome: AncillaRegister, - phase_flip_syndrome: AncillaRegister, - bit_flip_syndrome_measurement: ClassicalRegister, - phase_flip_syndrome_measurement: ClassicalRegister, -) -> None: - """Apply error correction based on syndrome measurements. - - Measures the 6 syndrome qubits and conditionally applies X/Z gates to correct - single-qubit errors on any of the 7 data qubits. - - Arguments: - qc: The quantum circuit to modify. - logical_qubit: Register containing the 7 data qubits. - bit_flip_syndrome: Register containing the 3 bit-flip syndrome qubits. - phase_flip_syndrome: Register containing the 3 phase-flip syndrome qubits. - bit_flip_syndrome_measurement: Classical register for bit-flip syndrome results. - phase_flip_syndrome_measurement: Classical register for phase-flip syndrome results. - """ - qc.measure(bit_flip_syndrome, bit_flip_syndrome_measurement) - qc.measure(phase_flip_syndrome, phase_flip_syndrome_measurement) - # Bit-flip correction: syndrome value directly indicates which qubit to correct - for i in range(7): - with qc.if_test((bit_flip_syndrome_measurement, i + 1)): - qc.x(logical_qubit[i]) - # Phase-flip correction: syndrome value directly indicates which qubit to correct - for i in range(7): - with qc.if_test((phase_flip_syndrome_measurement, i + 1)): - qc.z(logical_qubit[i]) +from ._registry import register_benchmark def _create_single_logical_qubit_circuit(index: int) -> QuantumCircuit: @@ -164,20 +51,20 @@ def _create_single_logical_qubit_circuit(index: int) -> QuantumCircuit: ) # == Encoding == qc.compose( - _get_seven_qubit_steane_code_encoding_circuit(), + get_seven_qubit_steane_code_encoding_circuit(), qubits=logical_qubit[:], inplace=True, ) qc.barrier() # == Syndrome extraction == qc.compose( - _get_seven_qubit_steane_code_syndrome_extraction_circuit(), + get_seven_qubit_steane_code_syndrome_extraction_circuit(), qubits=logical_qubit[:] + bit_flip_syndrome[:] + phase_flip_syndrome[:], inplace=True, ) qc.barrier() # == Error correction == - _apply_seven_qubit_steane_code_correction( + apply_seven_qubit_steane_code_correction( qc, logical_qubit, bit_flip_syndrome, @@ -188,7 +75,7 @@ def _create_single_logical_qubit_circuit(index: int) -> QuantumCircuit: qc.barrier() # == Decoding == qc.compose( - _get_seven_qubit_steane_code_decoding_circuit(), + get_seven_qubit_steane_code_decoding_circuit(), qubits=logical_qubit[:], inplace=True, ) diff --git a/src/mqt/bench/benchmarks/shors_nine_qubit_code.py b/src/mqt/bench/benchmarks/shors_nine_qubit_code.py index 5ea409ce7..ca728a28e 100644 --- a/src/mqt/bench/benchmarks/shors_nine_qubit_code.py +++ b/src/mqt/bench/benchmarks/shors_nine_qubit_code.py @@ -13,166 +13,17 @@ from qiskit import ClassicalRegister from qiskit.circuit import AncillaRegister, QuantumCircuit, QuantumRegister -from ._registry import register_benchmark - - -def _get_three_qubit_bit_flip_encoding_decoding_circuit() -> QuantumCircuit: - """Create 3-qubit bit-flip encoding/decoding circuit. - - Encodes |0> → |000> and |1> → |111>. Self-inverse, so used for both encoding and decoding. - - Returns: - QuantumCircuit: 3-qubit circuit (qubit 0 is the input/output qubit). - """ - out = QuantumCircuit(3) - out.cx(0, 1) - out.cx(0, 2) - return out - - -def _get_three_qubit_phase_flip_encoding_circuit() -> QuantumCircuit: - """Create 3-qubit phase-flip encoding circuit. - - Encodes |0> → |+++> and |1> → |---> - - Returns: - QuantumCircuit: 3-qubit encoding circuit (qubit 0 is the input qubit). - """ - out = QuantumCircuit(3) - out.cx(0, 1) - out.cx(0, 2) - out.h(0) - out.h(1) - out.h(2) - return out - - -def _get_three_qubit_phase_flip_decoding_circuit() -> QuantumCircuit: - """Create 3-qubit phase-flip decoding circuit. - - Reverses the phase-flip encoding. - - Returns: - QuantumCircuit: 3-qubit decoding circuit (qubit 0 is the output qubit). - """ - out = QuantumCircuit(3) - out.h(0) - out.h(1) - out.h(2) - out.cx(0, 1) - out.cx(0, 2) - return out - - -def _get_three_qubit_bit_flip_syndrome_extraction_circuit() -> QuantumCircuit: - """Create circuit to extract bit-flip syndrome from a 3-qubit block. - - Uses 2 ancilla qubits to measure parity and identify which qubit (if any) flipped. - Syndrome mapping: 01 → qubit 0, 10 → qubit 1, 11 → qubit 2, 00 → no error. +from mqt.bench.components.shor_circuit_components import ( + apply_nine_qubit_shors_code_bit_flip_correction, + apply_nine_qubit_shors_code_phase_flip_correction, + get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit, + get_three_qubit_bit_flip_encoding_decoding_circuit, + get_three_qubit_bit_flip_syndrome_extraction_circuit, + get_three_qubit_phase_flip_decoding_circuit, + get_three_qubit_phase_flip_encoding_circuit, +) - Returns: - QuantumCircuit: 5-qubit circuit (qubits 0-2 are data, qubits 3-4 are syndrome ancillas). - """ - out = QuantumCircuit(5) - out.cx(0, 3) - out.cx(1, 4) - out.cx(2, 3) - out.cx(2, 4) - return out - - -def _get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit() -> QuantumCircuit: - """Create circuit to extract phase-flip syndrome across the three 3-qubit blocks. - - Detects which block (if any) experienced a phase flip using 2 ancilla qubits. - Syndrome mapping: 01 → block 1 (qubits 0-2), 10 → block 2 (qubits 3-5), - 11 → block 3 (qubits 6-8), 00 → no error. - - Returns: - QuantumCircuit: 11-qubit circuit (qubits 0-8 are data, qubits 9-10 are syndrome ancillas). - """ - logical_qubit, phase_flip_syndrome = QuantumRegister(9), AncillaRegister(2) - out = QuantumCircuit(logical_qubit, phase_flip_syndrome) - # The order on the CNOT gates below is reversed when compared to what one might expect - # with the control being the ancilla, and the target being one of the component qubits of the logical qubit - # This is because we put Hadamards at the starts and ends of the ancilla bits, in order to check the phase - # of the logical qubits as opposed to the amplitude. - # But this also effectively swaps the order of the control and target, so we swap them back to normal - out.h(phase_flip_syndrome[0]) - out.h(phase_flip_syndrome[1]) - # Syndrome 01 (block 1) - for i in range(3): - out.cx(phase_flip_syndrome[0], logical_qubit[i]) - # Syndrome 10 (block 2) - for i in range(3, 6): - out.cx(phase_flip_syndrome[1], logical_qubit[i]) - # Syndrome 11 (block 3) - for i in range(6, 9): - out.cx(phase_flip_syndrome[0], logical_qubit[i]) - out.cx(phase_flip_syndrome[1], logical_qubit[i]) - out.h(phase_flip_syndrome[0]) - out.h(phase_flip_syndrome[1]) - return out - - -def _apply_nine_qubit_shors_code_bit_flip_correction( - qc: QuantumCircuit, - logical_qubit: QuantumRegister, - bit_flip_syndrome: AncillaRegister, - bit_flip_syndrome_measurement: ClassicalRegister, -) -> None: - """Apply bit-flip correction based on syndrome measurement. - - Measures the 6 syndrome qubits and conditionally applies X gates to correct - bit-flip errors on any of the 9 data qubits. - - Arguments: - qc: The quantum circuit to modify. - logical_qubit: Register containing the 9 data qubits. - bit_flip_syndrome: Ancilla register containing the 6 syndrome qubits. - bit_flip_syndrome_measurement: Classical register for syndrome measurement results. - """ - qc.measure(bit_flip_syndrome, bit_flip_syndrome_measurement) - # Note that Qiskit uses little-endian bit order - for index, syndrome in enumerate([ - 0b000001, - 0b000010, - 0b000011, - 0b000100, - 0b001000, - 0b001100, - 0b010000, - 0b100000, - 0b110000, - ]): - with qc.if_test((bit_flip_syndrome_measurement, syndrome)): - qc.x(logical_qubit[index]) - - -def _apply_nine_qubit_shors_code_phase_flip_correction( - qc: QuantumCircuit, - logical_qubit: QuantumRegister, - phase_flip_syndrome: AncillaRegister, - phase_flip_syndrome_measurement: ClassicalRegister, -) -> None: - """Apply phase-flip correction based on syndrome measurement. - - Measures the 2 syndrome qubits and conditionally applies Z gates to correct - phase-flip errors on the first qubit of the affected block. - - Arguments: - qc: The quantum circuit to modify. - logical_qubit: Register containing the 9 data qubits. - phase_flip_syndrome: Ancilla register containing the 2 syndrome qubits. - phase_flip_syndrome_measurement: Classical register for syndrome measurement results. - """ - qc.measure(phase_flip_syndrome, phase_flip_syndrome_measurement) - with qc.if_test((phase_flip_syndrome_measurement, 0b01)): - qc.z(logical_qubit[0]) - with qc.if_test((phase_flip_syndrome_measurement, 0b10)): - qc.z(logical_qubit[3]) - with qc.if_test((phase_flip_syndrome_measurement, 0b11)): - qc.z(logical_qubit[6]) +from ._registry import register_benchmark def _create_single_logical_qubit_circuit(index: int) -> QuantumCircuit: @@ -204,53 +55,51 @@ def _create_single_logical_qubit_circuit(index: int) -> QuantumCircuit: # == Encoding == # Apply phase flip encoding on the first qubit of each bit-flip block qc.compose( - _get_three_qubit_phase_flip_encoding_circuit(), + get_three_qubit_phase_flip_encoding_circuit(), qubits=[logical_qubit[0], logical_qubit[3], logical_qubit[6]], inplace=True, ) # Apply bit flip encoding on each block - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[:3], inplace=True) - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[3:6], inplace=True) - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[6:9], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[:3], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[3:6], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[6:9], inplace=True) qc.barrier() # == Syndrome extraction == qc.compose( - _get_three_qubit_bit_flip_syndrome_extraction_circuit(), + get_three_qubit_bit_flip_syndrome_extraction_circuit(), qubits=logical_qubit[:3] + bit_flip_syndrome[:2], inplace=True, ) qc.compose( - _get_three_qubit_bit_flip_syndrome_extraction_circuit(), + get_three_qubit_bit_flip_syndrome_extraction_circuit(), qubits=logical_qubit[3:6] + bit_flip_syndrome[2:4], inplace=True, ) qc.compose( - _get_three_qubit_bit_flip_syndrome_extraction_circuit(), + get_three_qubit_bit_flip_syndrome_extraction_circuit(), qubits=logical_qubit[6:9] + bit_flip_syndrome[4:6], inplace=True, ) qc.barrier() qc.compose( - _get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit(), + get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit(), qubits=logical_qubit[:] + phase_flip_syndrome[:], inplace=True, ) qc.barrier() # == Error correction == - _apply_nine_qubit_shors_code_bit_flip_correction( - qc, logical_qubit, bit_flip_syndrome, bit_flip_syndrome_measurement - ) + apply_nine_qubit_shors_code_bit_flip_correction(qc, logical_qubit, bit_flip_syndrome, bit_flip_syndrome_measurement) qc.barrier() - _apply_nine_qubit_shors_code_phase_flip_correction( + apply_nine_qubit_shors_code_phase_flip_correction( qc, logical_qubit, phase_flip_syndrome, phase_flip_syndrome_measurement ) qc.barrier() # == Decoding == - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[:3], inplace=True) - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[3:6], inplace=True) - qc.compose(_get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[6:9], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[:3], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[3:6], inplace=True) + qc.compose(get_three_qubit_bit_flip_encoding_decoding_circuit(), qubits=logical_qubit[6:9], inplace=True) qc.compose( - _get_three_qubit_phase_flip_decoding_circuit(), + get_three_qubit_phase_flip_decoding_circuit(), qubits=[logical_qubit[0], logical_qubit[3], logical_qubit[6]], inplace=True, ) @@ -279,17 +128,17 @@ def create_circuit(num_qubits: int) -> QuantumCircuit: Syndrome Extraction: - Bit-flip syndrome: For each block, 2 ancilla qubits measure the parity of - qubit pairs to detect which qubit (if any) experienced a bit flip. - Syndrome 01 → qubit 0, syndrome 10 → qubit 1, syndrome 11 → qubit 2. + qubit pairs to detect which qubit (if any) experienced a bit flip. + Syndrome 01 → qubit 0, syndrome 10 → qubit 1, syndrome 11 → qubit 2. - Phase-flip syndrome: 2 ancilla qubits detect phase differences between - the three blocks. Syndrome 01 → block 1 (qubits 0-2), syndrome 10 → block 2 - (qubits 3-5), syndrome 11 → block 3 (qubits 6-8). + the three blocks. Syndrome 01 → block 1 (qubits 0-2), syndrome 10 → block 2 + (qubits 3-5), syndrome 11 → block 3 (qubits 6-8). Error Correction: - Bit-flip correction: Based on the 6-bit syndrome measurement, X gates are - conditionally applied to correct bit flips on any of the 9 data qubits. + conditionally applied to correct bit flips on any of the 9 data qubits. - Phase-flip correction: Based on the 2-bit syndrome measurement, Z gates are - conditionally applied to the first qubit of the affected block. + conditionally applied to the first qubit of the affected block. Circuit Structure (per logical qubit): - 17 qubits: diff --git a/src/mqt/bench/components/shor_circuit_components.py b/src/mqt/bench/components/shor_circuit_components.py new file mode 100644 index 000000000..b7bf50ed1 --- /dev/null +++ b/src/mqt/bench/components/shor_circuit_components.py @@ -0,0 +1,177 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Shor's 9-qubit code circuit components.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qiskit.circuit import AncillaRegister, QuantumCircuit, QuantumRegister + +if TYPE_CHECKING: + from qiskit.circuit import ClassicalRegister + + +def get_three_qubit_phase_flip_decoding_circuit() -> QuantumCircuit: + """Create 3-qubit phase-flip decoding circuit. + + Reverses the phase-flip encoding. + + Returns: + QuantumCircuit: 3-qubit decoding circuit (qubit 0 is the output qubit). + """ + out = QuantumCircuit(3) + out.h(0) + out.h(1) + out.h(2) + out.cx(0, 1) + out.cx(0, 2) + return out + + +def get_three_qubit_bit_flip_encoding_decoding_circuit() -> QuantumCircuit: + """Create 3-qubit bit-flip encoding/decoding circuit. + + Encodes |0> → |000> and |1> → |111>. Self-inverse, so used for both encoding and decoding. + + Returns: + QuantumCircuit: 3-qubit circuit (qubit 0 is the input/output qubit). + """ + out = QuantumCircuit(3) + out.cx(0, 1) + out.cx(0, 2) + return out + + +def get_three_qubit_phase_flip_encoding_circuit() -> QuantumCircuit: + """Create 3-qubit phase-flip encoding circuit. + + Encodes |0> → |+++> and |1> → |---> + + Returns: + QuantumCircuit: 3-qubit encoding circuit (qubit 0 is the input qubit). + """ + out = QuantumCircuit(3) + out.cx(0, 1) + out.cx(0, 2) + out.h(0) + out.h(1) + out.h(2) + return out + + +def get_three_qubit_bit_flip_syndrome_extraction_circuit() -> QuantumCircuit: + """Create circuit to extract bit-flip syndrome from a 3-qubit block. + + Uses 2 ancilla qubits to measure parity and identify which qubit (if any) flipped. + Syndrome mapping: 01 → qubit 0, 10 → qubit 1, 11 → qubit 2, 00 → no error. + + Returns: + QuantumCircuit: 5-qubit circuit (qubits 0-2 are data, qubits 3-4 are syndrome ancillas). + """ + out = QuantumCircuit(5) + out.cx(0, 3) + out.cx(1, 4) + out.cx(2, 3) + out.cx(2, 4) + return out + + +def get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit() -> QuantumCircuit: + """Create circuit to extract phase-flip syndrome across the three 3-qubit blocks. + + Detects which block (if any) experienced a phase flip using 2 ancilla qubits. + Syndrome mapping: 01 → block 1 (qubits 0-2), 10 → block 2 (qubits 3-5), + 11 → block 3 (qubits 6-8), 00 → no error. + + Returns: + QuantumCircuit: 11-qubit circuit (qubits 0-8 are data, qubits 9-10 are syndrome ancillas). + """ + logical_qubit, phase_flip_syndrome = QuantumRegister(9), AncillaRegister(2) + out = QuantumCircuit(logical_qubit, phase_flip_syndrome) + # The order on the CNOT gates below is reversed when compared to what one might expect + # with the control being the ancilla, and the target being one of the component qubits of the logical qubit + # This is because we put Hadamards at the starts and ends of the ancilla bits, in order to check the phase + # of the logical qubits as opposed to the amplitude. + # But this also effectively swaps the order of the control and target, so we swap them back to normal + out.h(phase_flip_syndrome[0]) + out.h(phase_flip_syndrome[1]) + # Syndrome 01 (block 1) + for i in range(3): + out.cx(phase_flip_syndrome[0], logical_qubit[i]) + # Syndrome 10 (block 2) + for i in range(3, 6): + out.cx(phase_flip_syndrome[1], logical_qubit[i]) + # Syndrome 11 (block 3) + for i in range(6, 9): + out.cx(phase_flip_syndrome[0], logical_qubit[i]) + out.cx(phase_flip_syndrome[1], logical_qubit[i]) + out.h(phase_flip_syndrome[0]) + out.h(phase_flip_syndrome[1]) + return out + + +def apply_nine_qubit_shors_code_bit_flip_correction( + qc: QuantumCircuit, + logical_qubit: QuantumRegister, + bit_flip_syndrome: AncillaRegister, + bit_flip_syndrome_measurement: ClassicalRegister, +) -> None: + """Apply bit-flip correction based on syndrome measurement. + + Measures the 6 syndrome qubits and conditionally applies X gates to correct + bit-flip errors on any of the 9 data qubits. + + Arguments: + qc: The quantum circuit to modify. + logical_qubit: Register containing the 9 data qubits. + bit_flip_syndrome: Ancilla register containing the 6 syndrome qubits. + bit_flip_syndrome_measurement: Classical register for syndrome measurement results. + """ + qc.measure(bit_flip_syndrome, bit_flip_syndrome_measurement) + # Note that Qiskit uses little-endian bit order + for index, syndrome in enumerate([ + 0b000001, + 0b000010, + 0b000011, + 0b000100, + 0b001000, + 0b001100, + 0b010000, + 0b100000, + 0b110000, + ]): + with qc.if_test((bit_flip_syndrome_measurement, syndrome)): + qc.x(logical_qubit[index]) + + +def apply_nine_qubit_shors_code_phase_flip_correction( + qc: QuantumCircuit, + logical_qubit: QuantumRegister, + phase_flip_syndrome: AncillaRegister, + phase_flip_syndrome_measurement: ClassicalRegister, +) -> None: + """Apply phase-flip correction based on syndrome measurement. + + Measures the 2 syndrome qubits and conditionally applies Z gates to correct + phase-flip errors on the first qubit of the affected block. + + Arguments: + qc: The quantum circuit to modify. + logical_qubit: Register containing the 9 data qubits. + phase_flip_syndrome: Ancilla register containing the 2 syndrome qubits. + phase_flip_syndrome_measurement: Classical register for syndrome measurement results. + """ + qc.measure(phase_flip_syndrome, phase_flip_syndrome_measurement) + with qc.if_test((phase_flip_syndrome_measurement, 0b01)): + qc.z(logical_qubit[0]) + with qc.if_test((phase_flip_syndrome_measurement, 0b10)): + qc.z(logical_qubit[3]) + with qc.if_test((phase_flip_syndrome_measurement, 0b11)): + qc.z(logical_qubit[6]) diff --git a/src/mqt/bench/components/steane_circuit_components.py b/src/mqt/bench/components/steane_circuit_components.py new file mode 100644 index 000000000..296aead5d --- /dev/null +++ b/src/mqt/bench/components/steane_circuit_components.py @@ -0,0 +1,138 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Steane's 7-qubit code circuit components.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qiskit.circuit import AncillaRegister, QuantumCircuit, QuantumRegister + +if TYPE_CHECKING: + from qiskit.circuit import ClassicalRegister + + +def get_seven_qubit_steane_code_encoding_circuit() -> QuantumCircuit: + """Create the 7-qubit Steane code encoding circuit. + + Encodes qubit 0 into the 7-qubit Steane code logical state: + - |0> -> (|0000000> + |1010101> + |0110011> + |1100110> + |0001111> + |1011010> + |0111100> + |1101001>) + - |1> -> (|1111111> + |0101010> + |1001100> + |0011001> + |1110000> + |0100101> + |1000011> + |0010110>). + + Returns: + QuantumCircuit: 7-qubit encoding circuit. + """ + out = QuantumCircuit(7) + # H + out.h(4) + out.h(5) + out.h(6) + # CNOT from 0 + out.cx(0, 1) + out.cx(0, 2) + # CNOT from 6 + out.cx(6, 3) + out.cx(6, 1) + out.cx(6, 0) + # CNOT from 5 + out.cx(5, 3) + out.cx(5, 2) + out.cx(5, 0) + # CNOT from 4 + out.cx(4, 3) + out.cx(4, 2) + out.cx(4, 1) + return out + + +def get_seven_qubit_steane_code_decoding_circuit() -> QuantumCircuit: + """Create the 7-qubit Steane code decoding circuit. + + Reverses the encoding operation to extract the logical qubit back to qubit 0. + + Returns: + QuantumCircuit: 7-qubit decoding circuit (qubit 0 is the output qubit). + """ + return get_seven_qubit_steane_code_encoding_circuit().inverse() + + +def get_seven_qubit_steane_code_syndrome_extraction_circuit() -> QuantumCircuit: + """Create the syndrome extraction circuit for the 7-qubit Steane code. + + Extracts bit-flip and phase-flip syndromes using 6 ancilla qubits (3 for each type). + + Bit-flip syndrome extraction: + Syndrome bits measure the parity of specific qubit subsets corresponding to + the X-stabilizer generators. + + Phase-flip syndrome extraction: + Uses Hadamard gates to convert from Z to X basis, and control/target swapped + CNOTs to extract the phase-flip syndrome + + Syndrome mapping: The 3-bit syndrome value (1-7) directly identifies which + data qubit experienced an error. Syndrome 0 indicates no error. + + Returns: + QuantumCircuit: 13-qubit circuit (qubits 0-6 are data, 7-9 are bit-flip + syndrome ancillas, 10-12 are phase-flip syndrome ancillas). + """ + logical_qubit, bit_flip_syndrome, phase_flip_syndrome = QuantumRegister(7), AncillaRegister(3), AncillaRegister(3) + out = QuantumCircuit(logical_qubit, bit_flip_syndrome, phase_flip_syndrome) + # Bit-flip + for ctrl in (0, 2, 4, 6): + out.cx(logical_qubit[ctrl], bit_flip_syndrome[0]) + for ctrl in (1, 2, 5, 6): + out.cx(logical_qubit[ctrl], bit_flip_syndrome[1]) + for ctrl in (3, 4, 5, 6): + out.cx(logical_qubit[ctrl], bit_flip_syndrome[2]) + # Phase-flip + for i in range(3): + out.h(phase_flip_syndrome[i]) + for targ in (0, 2, 4, 6): + out.cx(phase_flip_syndrome[0], logical_qubit[targ]) + for targ in (1, 2, 5, 6): + out.cx(phase_flip_syndrome[1], logical_qubit[targ]) + for targ in (3, 4, 5, 6): + out.cx(phase_flip_syndrome[2], logical_qubit[targ]) + for i in range(3): + out.h(phase_flip_syndrome[i]) + return out + + +def apply_seven_qubit_steane_code_correction( + qc: QuantumCircuit, + logical_qubit: QuantumRegister, + bit_flip_syndrome: AncillaRegister, + phase_flip_syndrome: AncillaRegister, + bit_flip_syndrome_measurement: ClassicalRegister, + phase_flip_syndrome_measurement: ClassicalRegister, +) -> None: + """Apply error correction based on syndrome measurements. + + Measures the 6 syndrome qubits and conditionally applies X/Z gates to correct + single-qubit errors on any of the 7 data qubits. + + Arguments: + qc: The quantum circuit to modify. + logical_qubit: Register containing the 7 data qubits. + bit_flip_syndrome: Register containing the 3 bit-flip syndrome qubits. + phase_flip_syndrome: Register containing the 3 phase-flip syndrome qubits. + bit_flip_syndrome_measurement: Classical register for bit-flip syndrome results. + phase_flip_syndrome_measurement: Classical register for phase-flip syndrome results. + """ + qc.measure(bit_flip_syndrome, bit_flip_syndrome_measurement) + qc.measure(phase_flip_syndrome, phase_flip_syndrome_measurement) + # Bit-flip correction: syndrome value directly indicates which qubit to correct + for i in range(7): + with qc.if_test((bit_flip_syndrome_measurement, i + 1)): + qc.x(logical_qubit[i]) + # Phase-flip correction: syndrome value directly indicates which qubit to correct + for i in range(7): + with qc.if_test((phase_flip_syndrome_measurement, i + 1)): + qc.z(logical_qubit[i]) diff --git a/src/mqt/bench/error_correction/shor_transpiler.py b/src/mqt/bench/error_correction/shor_transpiler.py new file mode 100644 index 000000000..49ecc900b --- /dev/null +++ b/src/mqt/bench/error_correction/shor_transpiler.py @@ -0,0 +1,431 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Shor Transpiler for converting standard circuits into fault-tolerant circuits using the 9-qubit Shor code.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import numpy as np +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile +from qiskit.circuit import AncillaRegister + +# ignore the below comment +# these functions are reused from the benchmark and they should be extendable i.e. they shouldn't be private +from mqt.bench.components.shor_circuit_components import ( + apply_nine_qubit_shors_code_bit_flip_correction, + apply_nine_qubit_shors_code_phase_flip_correction, + get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit, + get_three_qubit_bit_flip_encoding_decoding_circuit, + get_three_qubit_bit_flip_syndrome_extraction_circuit, + get_three_qubit_phase_flip_encoding_circuit, +) + +if TYPE_CHECKING: + from collections.abc import Callable + +# Constants for the Shor 9-qubit code structure +SHOR_TOTAL_QUBITS = 9 +SHOR_BLOCK_SIZE = 3 +SHOR_NUM_BLOCKS = 3 +SHOR_PHASE_FLIP_TARGETS = [0, 3, 6] + + +@dataclass +class ShorLogicalQubit: + """Encapsulates the physical registers representing a single Shor logical qubit.""" + + data: QuantumRegister + bit_flip_syndrome: AncillaRegister | None = None + phase_flip_syndrome: AncillaRegister | None = None + bit_flip_measure: ClassicalRegister | None = None + phase_flip_measure: ClassicalRegister | None = None + + def get_all_registers(self) -> list: + """Return all active registers for this logical qubit.""" + regs = [self.data] + if self.bit_flip_syndrome: + regs.extend([ + self.bit_flip_syndrome, + self.phase_flip_syndrome, + self.bit_flip_measure, + self.phase_flip_measure, + ]) + return regs + + +class ShorTranspiler: + """A high-level transpiler that encodes a QuantumCircuit using Shor's 9-qubit error correction code.""" + + def __init__(self, original_circuit: QuantumCircuit, add_syndromes: bool = True) -> None: + """Initialize the transpiler with the original QuantumCircuit.""" + self.original_qc = original_circuit + self.num_logical_qubits = original_circuit.num_qubits + self.add_syndromes = add_syndromes + self.logical_qubits: list[ShorLogicalQubit] = [] + self.s_gate_count = 0 + self.t_gate_count = 0 + self.transpiled_qc = QuantumCircuit() + + # We need this for backwards compatibility with the testing suite + self.physical_data_registers: list[QuantumRegister] = [] + + def transpile(self) -> QuantumCircuit: + """Transpile the original circuit to a fault-tolerant circuit using Shor's code.""" + self.encode_qubits() + self.replace_gates() + return self.transpiled_qc + + def encode_qubits(self) -> None: + """Replace each logical qubit with a 9-qubit physical register and apply Shor encoding.""" + all_registers = [] + for i in range(self.num_logical_qubits): + data_reg = QuantumRegister(SHOR_TOTAL_QUBITS, f"q{i}") + self.physical_data_registers.append(data_reg) + + if self.add_syndromes: + logical_qubit = ShorLogicalQubit( + data=data_reg, + bit_flip_syndrome=AncillaRegister(6, f"bs{i}"), + phase_flip_syndrome=AncillaRegister(2, f"ps{i}"), + bit_flip_measure=ClassicalRegister(6, f"bsm{i}"), + phase_flip_measure=ClassicalRegister(2, f"psm{i}"), + ) + else: + logical_qubit = ShorLogicalQubit(data=data_reg) + + self.logical_qubits.append(logical_qubit) + all_registers.extend(logical_qubit.get_all_registers()) + + self.transpiled_qc = QuantumCircuit(*all_registers) + self.transpiled_qc.name = f"{self.original_qc.name}_shor_encoded" + + # Apply encoding for each logical qubit + for logical_qubit in self.logical_qubits: + self._apply_shor_encoding(self.transpiled_qc, logical_qubit.data) + self.transpiled_qc.barrier(label="Encoding") + + def decode_qubits(self) -> None: + """Apply Shor 9-qubit decoding to each logical qubit.""" + self.transpiled_qc.barrier() + for logical_qubit in self.logical_qubits: + self._apply_shor_decoding(self.transpiled_qc, logical_qubit.data) + self.transpiled_qc.barrier() + + @staticmethod + def _apply_shor_encoding(qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply Shor 9-qubit encoding to a physical data register.""" + # Phase flip encoding on the first qubit of each block + qc.compose( + get_three_qubit_phase_flip_encoding_circuit(), + qubits=[physical_data_register[i] for i in SHOR_PHASE_FLIP_TARGETS], + inplace=True, + ) + + # Bit flip encoding on each block + for i in range(SHOR_NUM_BLOCKS): + qc.compose( + get_three_qubit_bit_flip_encoding_decoding_circuit(), + qubits=physical_data_register[i * SHOR_BLOCK_SIZE : (i + 1) * SHOR_BLOCK_SIZE], + inplace=True, + ) + + @staticmethod + def _apply_shor_decoding(qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply Shor 9-qubit decoding to a physical data register.""" + for i in range(SHOR_NUM_BLOCKS): + qc.compose( + get_three_qubit_bit_flip_encoding_decoding_circuit().inverse(), + qubits=physical_data_register[i * SHOR_BLOCK_SIZE : (i + 1) * SHOR_BLOCK_SIZE], + inplace=True, + ) + qc.compose( + get_three_qubit_phase_flip_encoding_circuit().inverse(), + qubits=[physical_data_register[i] for i in SHOR_PHASE_FLIP_TARGETS], + inplace=True, + ) + + def replace_gates(self) -> None: + """Scan original circuit and replace gates with logical equivalents.""" + # Firstly, expand high level gates, such as QFTGate() + normalized = QuantumCircuit(*self.original_qc.qregs, *self.original_qc.cregs) + for instruction in self.original_qc.data: + gate_name = instruction.operation.name + + if gate_name == "qft": + tmp = QuantumCircuit(len(instruction.qubits)) + tmp.append(instruction.operation, range(len(instruction.qubits))) + + tmp = transpile( + tmp, + basis_gates=["h", "x", "z", "s", "t", "cx", "cz"], + optimization_level=3, + approximation_degree=0.95, + ) + + normalized.compose( + tmp, + qubits=list(instruction.qubits), + inplace=True, + ) + + else: + normalized.append( + instruction.operation, + instruction.qubits, + instruction.clbits, + ) + + self.original_qc = normalized + + for instruction in self.original_qc.data: + gate_name = instruction.operation.name + handler_name = f"_logical_{gate_name}" + + if not hasattr(self, handler_name): + msg = f"Gate {gate_name} is not supported by ShorTranspiler." + raise NotImplementedError(msg) + + handler = getattr(self, handler_name) + logical_qubit_indices = [self.original_qc.qubits.index(q) for q in instruction.qubits] + logical_clbit_indices = [self.original_qc.clbits.index(c) for c in instruction.clbits] + + if gate_name == "barrier": + handler(logical_qubit_indices) + elif gate_name == "measure": + handler(logical_qubit_indices[0], logical_clbit_indices[0]) + elif gate_name in ["cx", "cz"]: + handler(logical_qubit_indices[0], logical_qubit_indices[1]) + else: + handler(logical_qubit_indices[0]) + + def _logical_barrier(self, logical_qubit_indices: list[int]) -> None: + """Apply logical barrier across the specified physical qubits.""" + involved_physical_data_registers = [self.logical_qubits[idx].data for idx in logical_qubit_indices] + flattened_physical_qubits = [ + physical_qubit + for physical_data_register in involved_physical_data_registers + for physical_qubit in physical_data_register + ] + if flattened_physical_qubits: + self.transpiled_qc.barrier(flattened_physical_qubits) + else: + self.transpiled_qc.barrier() + + def _logical_measure(self, logical_qubit_index: int, logical_classical_bit_index: int) -> None: + """Apply logical measurement mapping to 9 physical measurements. + + Classical post-processing would compute the majority vote across the 3 bit-flip + blocks and then across the phase-flip blocks to extract the logical value. + """ + ## decode + self._apply_shor_decoding(self.transpiled_qc, self.logical_qubits[logical_qubit_index].data) + measurement_register_name = f"meas_{logical_qubit_index}_{logical_classical_bit_index}" + physical_measurement_register = ClassicalRegister(1, measurement_register_name) + self.transpiled_qc.add_register(physical_measurement_register) + + physical_data_register = self.logical_qubits[logical_qubit_index].data + self.transpiled_qc.measure(physical_data_register[0], physical_measurement_register[0]) + + def _logical_h(self, logical_qubit_index: int) -> None: + """Apply logical Hadamard. + + The Hadamard gate is not completely transversal for Shor's code. It requires + applying physical H gates followed by SWAPs that transpose the 9-qubit blocks. + """ + physical_data_register = self.logical_qubits[logical_qubit_index].data + for physical_qubit_index in range(SHOR_TOTAL_QUBITS): + self.transpiled_qc.h(physical_data_register[physical_qubit_index]) + # The Hadamard gate is not completely transversal for Shor's code. + # It needs to be followed by a swap that transposes the 9 qubits. + self.transpiled_qc.swap(physical_data_register[1], physical_data_register[3]) + self.transpiled_qc.swap(physical_data_register[2], physical_data_register[6]) + self.transpiled_qc.swap(physical_data_register[5], physical_data_register[7]) + self.insert_syndromes(logical_qubit_index) + + def _logical_x(self, logical_qubit_index: int) -> None: + """Apply Transversal logical X. + + In Shor's code, a logical X acts like a global physical Z across the three + blocks. Since Z on one qubit of a block flips the entire block's phase, + applying one Z per block (Z_0 Z_3 Z_6) transversally achieves logical X. + """ + physical_data_register = self.logical_qubits[logical_qubit_index].data + for q in (physical_data_register[i] for i in SHOR_PHASE_FLIP_TARGETS): + self.transpiled_qc.z(q) + self.insert_syndromes(logical_qubit_index) + + def _logical_z(self, logical_qubit_index: int) -> None: + """Apply Transversal logical Z. + + Applying X to the three qubits of a single block (e.g. X_0 X_1 X_2) maps + |000> to |111>, effectively giving diag(+1,-1) on the logical subspace. + """ + physical_data_register = self.logical_qubits[logical_qubit_index].data + for q in (physical_data_register[0], physical_data_register[1], physical_data_register[2]): + self.transpiled_qc.x(q) + self.insert_syndromes(logical_qubit_index) + + def _apply_teleportation_gadget( + self, + logical_qubit_index: int, + phase: float, + ancilla_name: str, + measure_name: str, + correction_callback: Callable, + ) -> None: + """Apply a magic state gate teleportation gadget (used for non-transversal S and T gates).""" + ancilla_register = QuantumRegister(SHOR_TOTAL_QUBITS, ancilla_name) + creg = ClassicalRegister(1, measure_name) + self.transpiled_qc.add_register(ancilla_register) + self.transpiled_qc.add_register(creg) + + physical_data_register = self.logical_qubits[logical_qubit_index].data + + # Prepare magic state: H -> P(phase) -> Encode + self._prepare_magic(self.transpiled_qc, ancilla_register, phase) + + # Transversal logical CNOT + self._apply_logical_cx(physical_data_register, ancilla_register) + + # Decode and measure ancilla in logical Z basis + self._apply_shor_decoding(self.transpiled_qc, ancilla_register) + self.transpiled_qc.measure(ancilla_register[0], creg[0]) + + # Apply conditional correction based on the measurement outcome + with self.transpiled_qc.if_test((creg[0], 1)): + correction_callback() + + self.insert_syndromes(logical_qubit_index) + + def _logical_s(self, logical_qubit_index: int) -> None: + """Apply logical S via |Y>-state teleportation. Correction: logical Z.""" + self.s_gate_count += 1 + + def z_correction() -> None: + self._logical_z(logical_qubit_index) + + self._apply_teleportation_gadget( + logical_qubit_index=logical_qubit_index, + phase=np.pi / 2, + ancilla_name=f"ms{self.s_gate_count - 1}", + measure_name=f"tmeas{self.s_gate_count - 1}", + correction_callback=z_correction, + ) + + def _logical_t(self, logical_qubit_index: int) -> None: + """Apply logical T via |A>-state teleportation. Correction: logical S.""" + self.t_gate_count += 1 + + def s_correction() -> None: + self._logical_s(logical_qubit_index) + + self._apply_teleportation_gadget( + logical_qubit_index=logical_qubit_index, + phase=np.pi / 4, + ancilla_name=f"anc_t_{self.t_gate_count}", + measure_name=f"creg_t_{self.t_gate_count}", + correction_callback=s_correction, + ) + + @staticmethod + def _prepare_magic(qc: QuantumCircuit, physical_ancilla_register: QuantumRegister, phase: float) -> None: + """Encode a magic state (|0> + e^{i*phase}|1>)/sqrt2 into a physical register.""" + qc.h(physical_ancilla_register[0]) + qc.p(phase, physical_ancilla_register[0]) + ShorTranspiler._apply_shor_encoding(qc, physical_ancilla_register) + + def _apply_logical_cx(self, control_register: QuantumRegister, target_register: QuantumRegister) -> None: + """Apply transversal logical CX between two physical registers.""" + for physical_qubit_index in range(SHOR_TOTAL_QUBITS): + self.transpiled_qc.cx(target_register[physical_qubit_index], control_register[physical_qubit_index]) + + def _logical_cx(self, control_logical_qubit_index: int, target_logical_qubit_index: int) -> None: + """Apply transversal logical CX. + + Because the Shor logical operators X_L and Z_L have interchanged physical basis mapping + compared to typical codes, the physical CX role is inverted: control and target are + swapped at the physical level to construct a logical CX. + """ + control_physical_data_register = self.logical_qubits[control_logical_qubit_index].data + target_physical_data_register = self.logical_qubits[target_logical_qubit_index].data + self._apply_logical_cx(control_physical_data_register, target_physical_data_register) + + self.insert_syndromes(control_logical_qubit_index) + self.insert_syndromes(target_logical_qubit_index) + + def _logical_cz(self, control_logical_qubit_index: int, target_logical_qubit_index: int) -> None: + """Apply logical CZ (implemented as H-CX-H).""" + self._logical_h(target_logical_qubit_index) + self.transpiled_qc.barrier() + self._logical_cx(control_logical_qubit_index, target_logical_qubit_index) + self.transpiled_qc.barrier() + self._logical_h(target_logical_qubit_index) + + def insert_syndromes(self, logical_qubit_index: int) -> None: + """Automate the insertion of bit-flip and phase-flip error correction cycles.""" + if not self.add_syndromes: + return + + qubit = self.logical_qubits[logical_qubit_index] + self.transpiled_qc.barrier() + + self._extract_bit_flip_syndromes(qubit) + self.transpiled_qc.barrier() + + self._extract_phase_flip_syndromes(qubit) + self.transpiled_qc.barrier() + + self._apply_error_corrections(qubit) + self.transpiled_qc.barrier() + + def _extract_bit_flip_syndromes(self, qubit: ShorLogicalQubit) -> None: + """Extract bit-flip syndromes for the three blocks.""" + if qubit.bit_flip_syndrome is None: + msg = "Bit-flip syndrome register is missing or not initialized." + raise ValueError(msg) + + self.transpiled_qc.reset(qubit.bit_flip_syndrome) + for i in range(SHOR_NUM_BLOCKS): + self.transpiled_qc.compose( + get_three_qubit_bit_flip_syndrome_extraction_circuit(), + qubits=qubit.data[i * SHOR_BLOCK_SIZE : (i + 1) * SHOR_BLOCK_SIZE] + + qubit.bit_flip_syndrome[i * 2 : (i + 1) * 2], + inplace=True, + ) + + def _extract_phase_flip_syndromes(self, qubit: ShorLogicalQubit) -> None: + """Extract phase-flip syndromes across the blocks.""" + if qubit.phase_flip_syndrome is None: + msg = "Bit-flip syndrome register is missing or not initialized." + raise ValueError(msg) + + self.transpiled_qc.reset(qubit.phase_flip_syndrome) + self.transpiled_qc.compose( + get_nine_qubit_shors_code_phase_flip_syndrome_extraction_circuit(), + qubits=qubit.data[:] + qubit.phase_flip_syndrome[:], + inplace=True, + ) + + def _apply_error_corrections(self, qubit: ShorLogicalQubit) -> None: + """Apply bit-flip and phase-flip error corrections based on syndromes.""" + apply_nine_qubit_shors_code_bit_flip_correction( + self.transpiled_qc, + qubit.data, + qubit.bit_flip_syndrome, + qubit.bit_flip_measure, + ) + self.transpiled_qc.barrier() + apply_nine_qubit_shors_code_phase_flip_correction( + self.transpiled_qc, + qubit.data, + qubit.phase_flip_syndrome, + qubit.phase_flip_measure, + ) diff --git a/src/mqt/bench/error_correction/steane_transpiler.py b/src/mqt/bench/error_correction/steane_transpiler.py new file mode 100644 index 000000000..9b015aa72 --- /dev/null +++ b/src/mqt/bench/error_correction/steane_transpiler.py @@ -0,0 +1,372 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Steane Transpiler for converting standard circuits into fault-tolerant circuits using the 7-qubit Steane code.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile +from qiskit.circuit import AncillaRegister +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes.synthesis import SolovayKitaev + +from mqt.bench.components.steane_circuit_components import ( + apply_seven_qubit_steane_code_correction, + get_seven_qubit_steane_code_decoding_circuit, + get_seven_qubit_steane_code_encoding_circuit, + get_seven_qubit_steane_code_syndrome_extraction_circuit, +) + +if TYPE_CHECKING: + from qiskit.circuit import CircuitInstruction + + +class SteaneTranspiler: + """A high-level transpiler that encodes a QuantumCircuit using Steane's 7-qubit error correction code.""" + + def __init__(self, original_circuit: QuantumCircuit, add_syndromes: bool = True) -> None: + """Initialize the transpiler with the original QuantumCircuit.""" + self.original_qc = original_circuit + self.num_logical_qubits = original_circuit.num_qubits + self.physical_data_registers: list[QuantumRegister] = [] + self.bit_flip_syndromes: list[AncillaRegister] = [] + self.phase_flip_syndromes: list[AncillaRegister] = [] + self.bit_flip_syndrome_measurements: list[ClassicalRegister] = [] + self.phase_flip_syndrome_measurements: list[ClassicalRegister] = [] + # self.logical_qubit_measurements: list[ClassicalRegister] = [] + self.add_syndromes = add_syndromes + self.t_gate_count = 0 + self.transpiled_qc = QuantumCircuit() + self.gate_handlers = { + "barrier": self._handle_barrier, + "measure": self._handle_measure, + "h": self._handle_h, + "x": self._handle_x, + "z": self._handle_z, + "s": self._handle_s, + "cx": self._handle_cx, + "cz": self._handle_cz, + "t": self._handle_t, + } + + def transpile(self) -> QuantumCircuit: + """Transpile the original circuit to a fault-tolerant circuit using Steane's code.""" + self.encode_qubits() + self.replace_gates() + return self.transpiled_qc + + def encode_qubits(self) -> None: + """Replace each logical qubit with a 7-qubit physical register and apply Steane encoding.""" + all_registers = [] + for logical_qubit_index in range(self.num_logical_qubits): + # use another name as logical_qubit + physical_data_register = QuantumRegister(7, f"q{logical_qubit_index}") + bit_flip_syndrome_register = AncillaRegister(3, f"bs{logical_qubit_index}") + phase_flip_syndrome_register = AncillaRegister(3, f"ps{logical_qubit_index}") + bit_flip_measurement_register = ClassicalRegister(3, f"bsm{logical_qubit_index}") + phase_flip_measurement_register = ClassicalRegister(3, f"psm{logical_qubit_index}") + # logical_qubit_measurement_register = ClassicalRegister(1, f"logical_meas{logical_qubit_index}") + + self.physical_data_registers.append(physical_data_register) + self.bit_flip_syndromes.append(bit_flip_syndrome_register) + self.phase_flip_syndromes.append(phase_flip_syndrome_register) + self.bit_flip_syndrome_measurements.append(bit_flip_measurement_register) + self.phase_flip_syndrome_measurements.append(phase_flip_measurement_register) + # self.logical_qubit_measurements.append(logical_qubit_measurement_register) + + all_registers.extend([ + physical_data_register, + bit_flip_syndrome_register, + phase_flip_syndrome_register, + bit_flip_measurement_register, + phase_flip_measurement_register, + # logical_qubit_measurement_register + ]) + + self.transpiled_qc = QuantumCircuit(*all_registers) + self.transpiled_qc.name = f"{self.original_qc.name}_steane_encoded" + + # Apply encoding for each logical qubit + for logical_qubit_index in range(self.num_logical_qubits): + physical_data_register = self.physical_data_registers[logical_qubit_index] + + # Phase flip encoding on the first qubit of each block + self.transpiled_qc.compose( + get_seven_qubit_steane_code_encoding_circuit(), + qubits=physical_data_register[:], + inplace=True, + ) + self.transpiled_qc.barrier(label="Encoding") + + def decode_qubits(self) -> None: + """Apply Steane 7-qubit decoding to each logical qubit.""" + self.transpiled_qc.barrier() + for logical_qubit_index in range(self.num_logical_qubits): + physical_data_register = self.physical_data_registers[logical_qubit_index] + self.transpiled_qc.compose( + get_seven_qubit_steane_code_decoding_circuit(), qubits=physical_data_register[:], inplace=True + ) + self.transpiled_qc.barrier() + + def replace_gates(self) -> None: + """Scan original circuit and replace gates with logical equivalents.""" + # Firstly, expand high level gates, such as QFTGate() + normalized = QuantumCircuit(*self.original_qc.qregs, *self.original_qc.cregs) + for instruction in self.original_qc.data: + gate_name = instruction.operation.name + + if gate_name == "qft": + tmp = QuantumCircuit(len(instruction.qubits)) + tmp.append(instruction.operation, range(len(instruction.qubits))) + + # 1. break down 2 qubit gates into single qubit gates, which are still continuous + continuous_basis = ["rx", "ry", "rz", "cx", "cz"] + tmp_continuous = transpile( + tmp, basis_gates=continuous_basis, optimization_level=3, approximation_degree=0.95 + ) + + # 2. Use Solovay-Kitaev to transform continuous gates into discrete ones + # recursion_degree controls depth/accuracy trade-off + sk_pass = SolovayKitaev(recursion_degree=2, basis_gates=["h", "x", "z", "s", "t"]) + pm = PassManager([sk_pass]) + pm.run(tmp_continuous) + + tmp = transpile( + tmp, + basis_gates=["h", "x", "z", "s", "t", "cx", "cz"], + optimization_level=3, + approximation_degree=0.95, + unitary_synthesis_method="sk", # test + ) + + normalized.compose( + tmp, + qubits=list(instruction.qubits), + inplace=True, + ) + + else: + normalized.append( + instruction.operation, + instruction.qubits, + instruction.clbits, + ) + + self.original_qc = normalized + + for instruction in self.original_qc.data: + gate_name = instruction.operation.name + if gate_name in self.gate_handlers: + self.gate_handlers[gate_name](instruction) + else: + msg = f"Gate {gate_name} is not supported by SteaneTranspiler." + raise NotImplementedError(msg) + + def _handle_barrier(self, instruction: CircuitInstruction) -> None: + """Handle barrier instruction.""" + barrier_register = [] + for i in range(len(instruction.qubits)): + physical_data_register = self.physical_data_registers[i] + bit_flip_syndromes_register = self.bit_flip_syndromes[i] + phase_flip_syndromes_register = self.phase_flip_syndromes[i] + barrier_register.extend([ + physical_data_register, + bit_flip_syndromes_register, + phase_flip_syndromes_register, + ]) + self.transpiled_qc.barrier(*barrier_register, label="Barrier") + + def _handle_measure(self, instruction: CircuitInstruction) -> None: + """Handle measure instruction.""" + # TODO: consider measure_all(), because of new meas register everything goes wrong + + for q, c in zip(instruction.qubits, instruction.clbits, strict=False): + logical_qubit_index = self.original_qc.qubits.index(q) + logical_classical_bit_index = self.original_qc.clbits.index(c) + + self.transpiled_qc.compose( + get_seven_qubit_steane_code_decoding_circuit(), + qubits=self.physical_data_registers[logical_qubit_index], + inplace=True, + ) + + measurement_register_name = f"meas_{logical_qubit_index}_{logical_classical_bit_index}" + physical_measurement_register = ClassicalRegister(1, measurement_register_name) + self.transpiled_qc.add_register(physical_measurement_register) + + physical_data_register = self.physical_data_registers[logical_qubit_index][0] + self.transpiled_qc.measure(physical_data_register, physical_measurement_register) + + # self.transpiled_qc.measure(self.physical_data_registers[logical_qubit_index][0], + # self.logical_qubit_measurements[logical_classical_bit_index]) + + self.transpiled_qc.barrier(label=f"Measurement {logical_qubit_index}") + + def _handle_h(self, instruction: CircuitInstruction) -> None: + """Handle Hadamard instruction.""" + logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + physical_data_register = self.physical_data_registers[logical_qubit_index] + + self.transpiled_qc.h(physical_data_register) + + self.transpiled_qc.barrier(label=f"H {logical_qubit_index}") + if self.add_syndromes: + self.insert_syndromes(logical_qubit_index) + + def _handle_x(self, instruction: CircuitInstruction) -> None: + """Handle X instruction.""" + logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + physical_data_register = self.physical_data_registers[logical_qubit_index] + + self.transpiled_qc.x(physical_data_register) + + self.transpiled_qc.barrier(label=f"X {logical_qubit_index}") + if self.add_syndromes: + self.insert_syndromes(logical_qubit_index) + + def _handle_z(self, instruction: CircuitInstruction) -> None: + """Handle Z instruction.""" + logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + physical_data_register = self.physical_data_registers[logical_qubit_index] + + self.transpiled_qc.z(physical_data_register) + + self.transpiled_qc.barrier(label=f"Z {logical_qubit_index}") + if self.add_syndromes: + self.insert_syndromes(logical_qubit_index) + + def _handle_s(self, instruction: CircuitInstruction) -> None: + """Handle S instruction.""" + # S Made cia SDG + logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + physical_data_register = self.physical_data_registers[logical_qubit_index] + + self.transpiled_qc.sdg(physical_data_register) + + self.transpiled_qc.barrier(label=f"S {logical_qubit_index}") + if self.add_syndromes: + self.insert_syndromes(logical_qubit_index) + + def _handle_t(self, instruction: CircuitInstruction) -> None: + """Handle T instruction.""" + logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + physical_data_register = self.physical_data_registers[logical_qubit_index] + + t_ancilla_register = AncillaRegister(7, f"t{self.t_gate_count}") + self.t_gate_count += 1 + t_test_register = ClassicalRegister(1) + + self.transpiled_qc.add_register(t_ancilla_register) + self.transpiled_qc.add_register(t_test_register) + + # make ket 0 L + self.transpiled_qc.compose( + get_seven_qubit_steane_code_encoding_circuit(), + qubits=t_ancilla_register[:], + inplace=True, + ) + + # make ket + L (Applying H L) + self.transpiled_qc.h(t_ancilla_register) + + # apply physical t gates + self.transpiled_qc.t(t_ancilla_register) + + # logical cnot from data to ancilla + self.transpiled_qc.cx(physical_data_register, t_ancilla_register) + + # made logical measurement + self.transpiled_qc.compose( + get_seven_qubit_steane_code_decoding_circuit(), qubits=t_ancilla_register, inplace=True + ) + self.transpiled_qc.measure(t_ancilla_register[0], t_test_register[0]) + + # Think about whether need to add error correction after these logical gates + + # apply if_test + with self.transpiled_qc.if_test((t_test_register[0], 1)): + self.transpiled_qc.sdg(physical_data_register) + + self.transpiled_qc.barrier(label=f"T {logical_qubit_index}") + if self.add_syndromes: + self.insert_syndromes(logical_qubit_index) + + def _handle_cx(self, instruction: CircuitInstruction) -> None: + """Handle CX instruction.""" + control_logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + target_logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[1]) + control_physical_data_register = self.physical_data_registers[control_logical_qubit_index] + target_physical_data_register = self.physical_data_registers[target_logical_qubit_index] + + self.transpiled_qc.cx( + control_physical_data_register, + target_physical_data_register, + ) + + self.transpiled_qc.barrier(label=f"CX {control_logical_qubit_index} {target_logical_qubit_index}") + + if self.add_syndromes: + self.insert_syndromes(control_logical_qubit_index) + self.insert_syndromes(target_logical_qubit_index) + + # it could use the hadamards with cnots + def _handle_cz(self, instruction: CircuitInstruction) -> None: + """Handle CZ instruction.""" + control_logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[0]) + target_logical_qubit_index = self.original_qc.qubits.index(instruction.qubits[1]) + control_physical_data_register = self.physical_data_registers[control_logical_qubit_index] + target_physical_data_register = self.physical_data_registers[target_logical_qubit_index] + + self.transpiled_qc.cz( + control_physical_data_register, + target_physical_data_register, + ) + + self.transpiled_qc.barrier(label=f"CZ {control_logical_qubit_index} {target_logical_qubit_index}") + + if self.add_syndromes: + self.insert_syndromes(control_logical_qubit_index) + self.insert_syndromes(target_logical_qubit_index) + + # TODO: Review and verify it works + def insert_syndromes(self, logical_qubit_index: int) -> None: + """Automate the insertion of the measurement and correction cycles.""" + physical_data_register = self.physical_data_registers[logical_qubit_index] + bit_flip_syndrome_register = self.bit_flip_syndromes[logical_qubit_index] + phase_flip_syndrome_register = self.phase_flip_syndromes[logical_qubit_index] + bit_flip_measurement_register = self.bit_flip_syndrome_measurements[logical_qubit_index] + phase_flip_measurement_register = self.phase_flip_syndrome_measurements[logical_qubit_index] + + self.transpiled_qc.barrier(label="Syndrome Start") + + # clean ancillas + self.transpiled_qc.reset(bit_flip_syndrome_register) + self.transpiled_qc.reset(phase_flip_syndrome_register) + + # Syndrome extraction + self.transpiled_qc.compose( + get_seven_qubit_steane_code_syndrome_extraction_circuit(), + qubits=physical_data_register[:] + bit_flip_syndrome_register[:] + phase_flip_syndrome_register[:], + inplace=True, + ) + + self.transpiled_qc.barrier() + + # Error correction + apply_seven_qubit_steane_code_correction( + self.transpiled_qc, + physical_data_register, + bit_flip_syndrome_register, + phase_flip_syndrome_register, + bit_flip_measurement_register, + phase_flip_measurement_register, + ) + + self.transpiled_qc.barrier(label="Correction End") diff --git a/tests/gate_counts.json b/tests/gate_counts.json new file mode 100644 index 000000000..17d2fb387 --- /dev/null +++ b/tests/gate_counts.json @@ -0,0 +1,561 @@ +{ + "shor": { + "ghz": { + "3": { + "cx": 186, + "if_else": 60, + "h": 47, + "measure": 43, + "reset": 40, + "barrier": 27, + "swap": 3 + }, + "4": { + "cx": 259, + "if_else": 84, + "h": 61, + "measure": 60, + "reset": 56, + "barrier": 37, + "swap": 3 + }, + "5": { + "cx": 332, + "if_else": 108, + "measure": 77, + "h": 75, + "reset": 72, + "barrier": 47, + "swap": 3 + }, + "6": { + "cx": 405, + "if_else": 132, + "measure": 94, + "h": 89, + "reset": 88, + "barrier": 57, + "swap": 3 + }, + "7": { + "cx": 478, + "if_else": 156, + "measure": 111, + "reset": 104, + "h": 103, + "barrier": 67, + "swap": 3 + }, + "8": { + "cx": 551, + "if_else": 180, + "measure": 128, + "reset": 120, + "h": 117, + "barrier": 77, + "swap": 3 + }, + "9": { + "cx": 624, + "if_else": 204, + "measure": 145, + "reset": 136, + "h": 131, + "barrier": 87, + "swap": 3 + } + }, + "bv": { + "3": { + "cx": 265, + "if_else": 108, + "h": 105, + "measure": 74, + "reset": 72, + "barrier": 48, + "swap": 18, + "z": 3 + }, + "4": { + "cx": 329, + "h": 137, + "if_else": 132, + "measure": 91, + "reset": 88, + "barrier": 58, + "swap": 24, + "z": 3 + }, + "5": { + "cx": 498, + "if_else": 204, + "h": 203, + "measure": 140, + "reset": 136, + "barrier": 90, + "swap": 36, + "z": 3 + }, + "6": { + "cx": 562, + "h": 235, + "if_else": 228, + "measure": 157, + "reset": 152, + "barrier": 100, + "swap": 42, + "z": 3 + }, + "7": { + "cx": 731, + "h": 301, + "if_else": 300, + "measure": 206, + "reset": 200, + "barrier": 132, + "swap": 54, + "z": 3 + }, + "8": { + "cx": 795, + "h": 333, + "if_else": 324, + "measure": 223, + "reset": 216, + "barrier": 142, + "swap": 60, + "z": 3 + }, + "9": { + "cx": 964, + "h": 399, + "if_else": 396, + "measure": 272, + "reset": 264, + "barrier": 174, + "swap": 72, + "z": 3 + } + }, + "graphstate": { + "3": { + "cx": 435, + "if_else": 180, + "h": 159, + "measure": 123, + "reset": 120, + "barrier": 83, + "swap": 27 + }, + "4": { + "cx": 580, + "if_else": 240, + "h": 212, + "measure": 164, + "reset": 160, + "barrier": 110, + "swap": 36 + }, + "5": { + "cx": 725, + "if_else": 300, + "h": 265, + "measure": 205, + "reset": 200, + "barrier": 137, + "swap": 45 + }, + "6": { + "cx": 870, + "if_else": 360, + "h": 318, + "measure": 246, + "reset": 240, + "barrier": 164, + "swap": 54 + }, + "7": { + "cx": 1015, + "if_else": 420, + "h": 371, + "measure": 287, + "reset": 280, + "barrier": 191, + "swap": 63 + }, + "8": { + "cx": 1160, + "if_else": 480, + "h": 424, + "measure": 328, + "reset": 320, + "barrier": 218, + "swap": 72 + }, + "9": { + "cx": 1305, + "if_else": 540, + "h": 477, + "measure": 369, + "reset": 360, + "barrier": 245, + "swap": 81 + } + }, + "qft": { + "3": { + "cx": 788, + "if_else": 260, + "h": 185, + "measure": 179, + "reset": 168, + "barrier": 107, + "swap": 9, + "p": 8, + "x": 6 + }, + "4": { + "cx": 1162, + "if_else": 384, + "h": 268, + "measure": 264, + "reset": 248, + "barrier": 157, + "swap": 12, + "p": 12, + "x": 9 + }, + "5": { + "cx": 1536, + "if_else": 508, + "h": 351, + "measure": 349, + "reset": 328, + "barrier": 207, + "p": 16, + "swap": 15, + "x": 12 + }, + "6": { + "cx": 1910, + "if_else": 632, + "h": 434, + "measure": 434, + "reset": 408, + "barrier": 257, + "p": 20, + "swap": 18, + "x": 15 + }, + "7": { + "cx": 2284, + "if_else": 756, + "measure": 519, + "h": 517, + "reset": 488, + "barrier": 307, + "p": 24, + "swap": 21, + "x": 18 + }, + "8": { + "cx": 2658, + "if_else": 880, + "measure": 604, + "h": 600, + "reset": 568, + "barrier": 357, + "p": 28, + "swap": 24, + "x": 21 + }, + "9": { + "cx": 3032, + "if_else": 1004, + "measure": 689, + "h": 683, + "reset": 648, + "barrier": 407, + "p": 32, + "swap": 27, + "x": 24 + } + } + }, + "steane": { + "ghz": { + "3": { + "cx": 200, + "if_else": 70, + "h": 55, + "measure": 33, + "reset": 30, + "barrier": 23 + }, + "4": { + "cx": 277, + "if_else": 98, + "h": 73, + "measure": 46, + "reset": 42, + "barrier": 31 + }, + "5": { + "cx": 354, + "if_else": 126, + "h": 91, + "measure": 59, + "reset": 54, + "barrier": 39 + }, + "6": { + "cx": 431, + "if_else": 154, + "h": 109, + "measure": 72, + "reset": 66, + "barrier": 47 + }, + "7": { + "cx": 508, + "if_else": 182, + "h": 127, + "measure": 85, + "reset": 78, + "barrier": 55 + }, + "8": { + "cx": 585, + "if_else": 210, + "h": 145, + "measure": 98, + "reset": 90, + "barrier": 63 + }, + "9": { + "cx": 662, + "if_else": 238, + "h": 163, + "measure": 111, + "reset": 102, + "barrier": 71 + } + }, + "bv": { + "3": { + "cx": 223, + "if_else": 98, + "h": 85, + "measure": 44, + "reset": 42, + "barrier": 30, + "x": 7, + "cz": 7 + }, + "4": { + "cx": 293, + "if_else": 126, + "h": 117, + "measure": 57, + "reset": 54, + "barrier": 39, + "x": 7, + "cz": 7 + }, + "5": { + "cx": 411, + "if_else": 182, + "h": 161, + "measure": 82, + "reset": 78, + "barrier": 55, + "cz": 14, + "x": 7 + }, + "6": { + "cx": 481, + "if_else": 210, + "h": 193, + "measure": 95, + "reset": 90, + "barrier": 64, + "cz": 14, + "x": 7 + }, + "7": { + "cx": 599, + "if_else": 266, + "h": 237, + "measure": 120, + "reset": 114, + "barrier": 80, + "cz": 21, + "x": 7 + }, + "8": { + "cx": 669, + "if_else": 294, + "h": 269, + "measure": 133, + "reset": 126, + "barrier": 89, + "cz": 21, + "x": 7 + }, + "9": { + "cx": 787, + "if_else": 350, + "h": 313, + "measure": 158, + "reset": 150, + "barrier": 105, + "cz": 28, + "x": 7 + } + }, + "graphstate": { + "3": { + "cx": 282, + "if_else": 126, + "h": 93, + "measure": 57, + "reset": 54, + "barrier": 38, + "cz": 21 + }, + "4": { + "cx": 376, + "if_else": 168, + "h": 124, + "measure": 76, + "reset": 72, + "barrier": 50, + "cz": 28 + }, + "5": { + "cx": 470, + "if_else": 210, + "h": 155, + "measure": 95, + "reset": 90, + "barrier": 62, + "cz": 35 + }, + "6": { + "cx": 564, + "if_else": 252, + "h": 186, + "measure": 114, + "reset": 108, + "barrier": 74, + "cz": 42 + }, + "7": { + "cx": 658, + "if_else": 294, + "h": 217, + "measure": 133, + "reset": 126, + "barrier": 86, + "cz": 49 + }, + "8": { + "cx": 752, + "if_else": 336, + "h": 248, + "measure": 152, + "reset": 144, + "barrier": 98, + "cz": 56 + }, + "9": { + "cx": 846, + "if_else": 378, + "h": 279, + "measure": 171, + "reset": 162, + "barrier": 110, + "cz": 63 + } + }, + "qft": { + "3": { + "cx": 772, + "if_else": 300, + "h": 243, + "measure": 135, + "reset": 126, + "barrier": 85, + "t": 42, + "sdg": 14, + "z": 14 + }, + "4": { + "cx": 1135, + "if_else": 443, + "h": 355, + "measure": 199, + "reset": 186, + "barrier": 124, + "t": 63, + "sdg": 21, + "z": 21 + }, + "5": { + "cx": 1498, + "if_else": 586, + "h": 467, + "measure": 263, + "reset": 246, + "barrier": 163, + "t": 84, + "sdg": 28, + "z": 28 + }, + "6": { + "cx": 1861, + "if_else": 729, + "h": 579, + "measure": 327, + "reset": 306, + "barrier": 202, + "t": 105, + "sdg": 35, + "z": 35 + }, + "7": { + "cx": 2224, + "if_else": 872, + "h": 691, + "measure": 391, + "reset": 366, + "barrier": 241, + "t": 126, + "sdg": 42, + "z": 42 + }, + "8": { + "cx": 2587, + "if_else": 1015, + "h": 803, + "measure": 455, + "reset": 426, + "barrier": 280, + "t": 147, + "sdg": 49, + "z": 49 + }, + "9": { + "cx": 2950, + "if_else": 1158, + "h": 915, + "measure": 519, + "reset": 486, + "barrier": 319, + "t": 168, + "sdg": 56, + "z": 56 + } + } + } +} diff --git a/tests/test_error_correction.py b/tests/test_error_correction.py new file mode 100644 index 000000000..3386eb283 --- /dev/null +++ b/tests/test_error_correction.py @@ -0,0 +1,565 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the error-correction transpilers (Steane and Shor codes).""" + +from __future__ import annotations + +import json +from pathlib import Path +from re import fullmatch +from typing import TYPE_CHECKING + +import mqt.qcec +import pytest +from mqt.qcec.pyqcec import EquivalenceCriterion +from qiskit import QuantumCircuit +from qiskit.circuit import CircuitInstruction, ClassicalRegister +from qiskit.circuit.library import CXGate, CZGate, HGate, SGate, XGate, ZGate +from qiskit.quantum_info import hellinger_fidelity +from qiskit_aer.primitives import SamplerV2 + +from mqt.bench import benchmark_generation +from mqt.bench.error_correction.shor_transpiler import ShorTranspiler +from mqt.bench.error_correction.steane_transpiler import SteaneTranspiler + +if TYPE_CHECKING: + import qiskit as qk + from qiskit.circuit import Gate + + +@pytest.mark.parametrize("code", ["steane", "shor"]) +@pytest.mark.parametrize("gate", [XGate(), ZGate(), HGate(), SGate()]) +def test_errorcorrection_transpiler_gate_equivalence(code: str, gate: Gate) -> None: + """Verify that the error-correction transpiler preserves gate semantics. + + For each supported single-qubit gate, builds a minimal logical circuit + containing only that gate, transpiles it with syndrome extraction disabled, + decodes the physical qubits back to logical qubits, and then checks via + MQT QCEC that the resulting circuit is unitarily equivalent to the original. + """ + if gate.name == "s" and code == "shor": + # this transpiler constructs the SGate using measure and a classically controlled operation + # therefore it can't be evaluated properly by MQT.QCEC + return + + num_qubits = gate.num_qubits + logical_circuit = QuantumCircuit(num_qubits) + logical_circuit.append(gate, qargs=list(range(num_qubits))) + + error_corrected_circuit = logical_circuit.copy() + if code == "shor": + transpiler = ShorTranspiler(error_corrected_circuit, add_syndromes=False) + else: + transpiler = SteaneTranspiler(error_corrected_circuit, add_syndromes=False) + transpiler.transpile() + transpiler.decode_qubits() + error_corrected_circuit = transpiler.transpiled_qc + + assert check_equivalence(logical_circuit, error_corrected_circuit), ( + f"Transpiler {code} does not convert Gate {gate.name} to its logical equivalent" + ) + + +@pytest.mark.parametrize("code", ["steane", "shor"]) +@pytest.mark.parametrize("gate", [XGate(), ZGate(), HGate(), SGate(), CXGate(), CZGate()]) +def test_errorcorrection_transpiler_gate_correctness(code: str, gate: Gate) -> None: + """Verify that the transpiler actually corrects an introduced bit-flip error. + + Builds a minimal single-gate logical circuit, transpiles it with full syndrome + extraction enabled, and then creates a copy in which a bit-flip (X gate) is + injected after the first barrier. Both the clean and the error-induced circuits + are simulated, and the test asserts that: + + 1. The error-corrected circuit matches the logical circuit. + 2. The error-induced circuit still matches the clean corrected circuit. + """ + if gate.name == "s" and code == "shor": + # this takes a little longer.... + return + + num_qubits = gate.num_qubits + logical_circuit = QuantumCircuit(num_qubits) + logical_circuit.append(gate, qargs=list(range(num_qubits))) + error_corrected_circuit = logical_circuit.copy() + if code == "shor": + transpiler = ShorTranspiler(error_corrected_circuit, add_syndromes=True) + else: + transpiler = SteaneTranspiler(error_corrected_circuit, add_syndromes=True) + transpiler.transpile() + transpiler.decode_qubits() + error_corrected_circuit = transpiler.transpiled_qc + + error_induced_circuit = error_corrected_circuit.copy() + # this is for inserting phase flip in steane after the first Hadamard + error_induced_circuit = insert_error(error_induced_circuit, gate=XGate()) + + logical_counts, logical_circuit = run_circuit(logical_circuit) + corrected_counts, error_corrected_circuit = run_circuit(error_corrected_circuit) + induced_counts, error_induced_circuit = run_circuit(error_induced_circuit) + + logical_corrected_fidelity = compare_distributions( + logical_circuit, error_corrected_circuit, logical_counts, corrected_counts, "none", code + ) + corrected_induced_fidelity = compare_distributions( + error_corrected_circuit, error_induced_circuit, corrected_counts, induced_counts, code, code + ) + + assert logical_corrected_fidelity >= 0.99, ( + f"Error corrected circuit created by {code} transpiler for Gate {gate.name} does not match its logical circuit well enough." + ) + assert corrected_induced_fidelity >= 0.99, ( + f"Error corrected circuit created by {code} transpiler for Gate {gate.name} does not correct the bitflip well enough." + ) + + +def add_h_before_measurements(qc: QuantumCircuit) -> QuantumCircuit: + """Return a copy of *qc* with an H gate inserted before every measurement. + + This switches each qubit from the Z basis to the X basis immediately prior + to measurement, which is useful for testing circuits whose final state lies + along the X axis of the Bloch sphere (e.g. circuits ending in a superposition). + + Args: + qc: The source circuit; it is not modified in place. + + Returns: + A new :class:`~qiskit.QuantumCircuit` with the same registers and + instructions as *qc*, but with an H gate prepended to every measure + operation. + """ + new_qc = QuantumCircuit(*qc.qregs, *qc.cregs, name=qc.name) + + for instruction in qc.data: + op = instruction.operation + qargs = instruction.qubits + cargs = instruction.clbits + + if op.name == "measure": + # Add H to the qubit that is about to be measured + new_qc.h(qargs[0]) + + # Add the original instruction + new_qc.append(op, qargs, cargs) + + return new_qc + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +@pytest.mark.parametrize("algorithm", ["ghz", "bv", "graphstate"]) # "qft" is unfeasible: >3k gates on 34 qubits +@pytest.mark.parametrize("error", [XGate(), ZGate()]) +@pytest.mark.parametrize("measure_base_x", [True, False]) +@pytest.mark.parametrize("circuit_size", [3]) +def test_errorcorrection_transpiler_correctness( + code: str, algorithm: str, error: Gate, measure_base_x: bool, circuit_size: int +) -> None: + """Ensures the transpiler creates error-corrected circuits which produce the same result as the original logical circuit. + + Afterwards an error is introduced and the test checks, whether it is corrected. + Iterates over a number of example algorithms. + + `circuit_size` can be any list of integers between 3 and 10 (=> up to range(3,11)). + For larger circuit sizes, gate_counts.json has to be updated + """ + test_id = f"{circuit_size} qubit {algorithm} on {code} with ZBasis {measure_base_x} and error {error.name}" + + # Initialize circuits + logical_circuit = benchmark_generation.get_benchmark( + benchmark=algorithm, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=circuit_size, encoding="" + ) + + if measure_base_x: + logical_circuit = add_h_before_measurements(logical_circuit) + + # Strip measure gates to avoid intermediate measurements collapsing the state before decoding + stripped_logical_circuit = QuantumCircuit(*logical_circuit.qregs, *logical_circuit.cregs) + for inst in logical_circuit.data: + if inst.operation.name != "measure": + stripped_logical_circuit.append(inst.operation, inst.qubits, inst.clbits) + logical_circuit = stripped_logical_circuit + + if code == "shor": + transpiler = ShorTranspiler(logical_circuit.copy(), add_syndromes=True) + else: + transpiler = SteaneTranspiler(logical_circuit.copy(), add_syndromes=True) + transpiler.transpile() + transpiler.decode_qubits() + error_corrected_circuit = transpiler.transpiled_qc + + error_induced_circuit = error_corrected_circuit.copy() + error_induced_circuit = insert_error_after_barrier( + error_corrected_circuit, + barrier_label="Encoding", + gate=error, + qubit_index=0, + ) + + logical_counts, logical_circuit = run_circuit(logical_circuit) + corrected_counts, error_corrected_circuit = run_circuit(error_corrected_circuit) + induced_counts, error_induced_circuit = run_circuit(error_induced_circuit) + + logical_corrected_fidelity = compare_distributions( + logical_circuit, error_corrected_circuit, logical_counts, corrected_counts, "none", code + ) + corrected_induced_fidelity = compare_distributions( + error_corrected_circuit, error_induced_circuit, corrected_counts, induced_counts, code, code + ) + + assert logical_corrected_fidelity >= 0.99, ( + f"Error corrected circuit created does not match its logical circuit well enough for {test_id}" + ) + assert corrected_induced_fidelity >= 0.99, ( + f"Error induced circuit created does not match correct the error well enough for {test_id}" + ) + + +@pytest.mark.parametrize("logical_qubits", range(3, 10)) # multiple parametrize lead to crossproducts +@pytest.mark.parametrize("alg", ["ghz", "bv", "graphstate", "qft"]) +@pytest.mark.parametrize("code", ["shor", "steane"]) +def test_error_correction_circuit_structure(code: str, alg: str, logical_qubits: int) -> None: + """Verify the physical circuit structure produced by the error-correction encoder. + + Checks that the encoded circuit has the correct number of physical qubits, + classical bits, and register sizes for the given code and algorithm, and that + the exact gate counts match the reference values stored in ``gate_counts.json``. + + The expected qubit and classical-bit counts are code-dependent: + + * **Steane code**: 13 physical qubits per logical qubit (7 data + 3 bit-flip + ancilla + 3 phase-flip ancilla) and 6 classical bits per logical qubit (3 + bit-flip syndrome + 3 phase-flip syndrome), plus one bit per original clbit. + * **Shor code**: 17 physical qubits per logical qubit (9 data + 6 Z-stabiliser + ancilla + 2 X-stabiliser ancilla) and 8 classical bits per logical qubit (6 + bit-flip syndrome + 2 phase-flip syndrome), plus one bit per original clbit. + + QFT circuits are excluded from the qubit-count checks because their ancilla + qubit count scales with the number of T gates rather than the logical qubit count. + """ + test_id = f"{logical_qubits} qubit {alg} on {code}" + + qc = benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=logical_qubits, encoding=code + ) + log_qc = benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=logical_qubits, encoding="" + ) + + qubit_code_factor = -1 + classical_code_factor = -1 + + expected_qreg_sizes = [] + expected_creg_sizes = [] + + if code == "steane": + # Each logical qubit is split in 7 physical qubits + # Additionally, 6 ancillary registers are added + qubit_code_factor = 13 + + classical_code_factor = 6 + + # Check quantum register sizes: 7n (data) + 3n (bit-flip syndrome) + 3n (phase-flip syndrome) + expected_qreg_sizes = sorted([7] * logical_qubits + [3] * logical_qubits + [3] * logical_qubits) + + # Check classical register sizes: 3n (bit-flip) + 3n (phase-flip) + 1 for each original clbit + expected_creg_sizes = sorted([3] * logical_qubits + [3] * logical_qubits + [1] * log_qc.num_clbits) + elif code == "shor": + # Each logical qubit is split in 9 physical qubits + # Additionally, 8 ancilla qubits are added as stabilisers (6Z + 2X) + # => 1 logical qubit = 17 physical qubits + qubit_code_factor = 17 + # Each ancilla requires 1 clbit for syndrome extraction => 6*2 = 8 + classical_code_factor = 8 + + # Check quantum register sizes: 9n (data) + 6n (bit-flip syndrome) + 2n (phase-flip syndrome) + expected_qreg_sizes = sorted([9] * logical_qubits + [6] * logical_qubits + [2] * logical_qubits) + + # Check classical register sizes: 6n (bit-flip) + 2n (phase-flip) + 1 for each original clbit + expected_creg_sizes = sorted([6] * logical_qubits + [2] * logical_qubits + [1] * log_qc.num_clbits) + + # QFT creates qubits scaling with the number of t-gates -> non-trivial scaling not covered by these simple tests + if alg != "qft": + expected_qubits = qubit_code_factor * log_qc.num_qubits + found_qubits = qc.num_qubits + assert found_qubits == expected_qubits, f"Expected {expected_qubits} qubits, found {found_qubits} for {test_id}" + + expected_clbits = classical_code_factor * log_qc.num_qubits + log_qc.num_clbits + found_clbits = qc.num_clbits + assert found_clbits == expected_clbits, ( + f"Expected {expected_clbits} classical bits, found {found_clbits} for {test_id}" + ) + + qreg_sizes = sorted(qreg.size for qreg in qc.qregs) + assert qreg_sizes == expected_qreg_sizes, ( + f"Expected qreg sizes {expected_qreg_sizes}, found {qreg_sizes} for {test_id}" + ) + + creg_sizes = sorted(creg.size for creg in qc.cregs) + assert creg_sizes == expected_creg_sizes, ( + f"Expected creg sizes {expected_creg_sizes}, found {creg_sizes} for {test_id}" + ) + + expected_gate_counts = None + + json_location = Path(__file__).parent / "gate_counts.json" + with json_location.open("r", encoding="utf-8") as json_data: + expected_gate_counts = json.load(json_data) + + assert expected_gate_counts is not None, f"Failure reading respective gate counts for {test_id}" + expected_gate_counts = expected_gate_counts[code][alg][f"{logical_qubits}"] + + # Counts the occurrence of every gate in the created circuit + created_gate_counts = qc.count_ops() + assert expected_gate_counts == created_gate_counts, ( + f"Created circuit does not contain the expected gates for {test_id}" + ) + + +def insert_error_after_barrier( + qc: QuantumCircuit, + barrier_label: str, + gate: Gate | None = None, + qubit_index: int = 0, +) -> QuantumCircuit: + """Insert a fault gate immediately after the first barrier with a given label. + + Scans *qc* for a barrier whose ``.label`` attribute matches *barrier_label* + and inserts *gate* on the qubit at *qubit_index* directly after it. This + allows tests to inject a well-placed error (e.g. right after the encoding + barrier) without disturbing the rest of the circuit structure. + + Args: + qc: The circuit to inject the error into. A shallow copy is made so + the original is not modified. + barrier_label: The label of the barrier after which the gate is inserted. + gate: The fault gate to inject. Defaults to :class:`~qiskit.circuit.library.XGate` + (a bit flip) if ``None``. + qubit_index: Index into ``qc.qubits`` of the qubit to apply the gate to. + Defaults to ``0``. + + Returns: + A copy of *qc* with the error gate inserted. + + Raises: + ValueError: If no barrier with *barrier_label* is found in the circuit. + """ + gate = XGate() if gate is None else gate + + qc = qc.copy() + + for i, instruction in enumerate(qc.data): + if instruction.operation.name == "barrier" and instruction.operation.label == barrier_label: + qc.data.insert( + i + 1, + CircuitInstruction(gate, [qc.qubits[qubit_index]]), + ) + return qc + + msg = f"Barrier with label {barrier_label!r} not found" + raise ValueError(msg) + + +def insert_error(qc: QuantumCircuit, gate: Gate | None = None, index: int | None = None) -> QuantumCircuit: + """Insert a fault gate right after the first barrier in *qc*. + + Locates the first :class:`~qiskit.circuit.Barrier` instruction and inserts + *gate* immediately after it on the first ``gate.num_qubits`` qubits. An + explicit *index* can be provided to override the barrier-search behaviour. + + Args: + qc: The circuit to inject the error into (modified in place). + gate: The fault gate to inject. Defaults to + :class:`~qiskit.circuit.library.XGate` (a bit flip) if ``None``. + index: Instruction index at which to insert the gate. If ``None`` + (default), the position right after the first barrier is used. + + Returns: + The same *qc* instance with the error gate inserted. + + Raises: + ValueError: If *index* is ``None`` and no barrier is found in the circuit. + """ + gate = XGate() if gate is None else gate + if qc.num_qubits < gate.num_qubits: + msg = f"Quantum Circuit has not enough qubits to accommodate gate {gate.name}" + raise ValueError(msg) + if index is not None and index < 0: + msg = f"Index must be >= 0, Index provided: {index}" + raise ValueError(msg) + + # Finds the first barrier + if index is None: + for i, instruction in enumerate(qc.data): + if instruction.operation.name == "barrier": + index = i + 1 + break + + # Insert the error gate + qubits = qc.qubits[: gate.num_qubits] + if index is not None: + qc.data.insert(index, CircuitInstruction(gate, qubits)) + else: + msg = "Please provide either an index or a circuit with a barrier to insert an error into" + raise ValueError(msg) + + return qc + + +def check_equivalence(qc1: qk.QuantumCircuit, qc2: qk.QuantumCircuit) -> bool: + """Uses MQT QCEC to verify if qc1 and qc2 are equivalent. + + Args: + qc1: The first quantum circuit. + qc2: The second quantum circuit. + + Returns: + True if the circuits are equivalent, equivalent up to global phase, + or probably equivalent; False otherwise. + """ + verification_results = mqt.qcec.verify(qc1, qc2, check_partial_equivalence=True) + accepted_equivalencies = [ + EquivalenceCriterion.equivalent, + EquivalenceCriterion.equivalent_up_to_global_phase, + EquivalenceCriterion.probably_equivalent, + ] + return verification_results.equivalence in accepted_equivalencies + + +def measure_all_named(qc: QuantumCircuit, name: str = "measurement") -> QuantumCircuit: + """Add a named classical register to *qc* and measure every qubit into it. + + Creates a :class:`~qiskit.circuit.ClassicalRegister` of width + ``qc.num_qubits``, appends it to *qc*, and maps qubit *i* to bit *i* of that + register. This is a convenience wrapper used by :func:`run_circuit` to attach + measurements before simulation while keeping the register name predictable for + later result extraction. + + Args: + qc: The circuit to add measurements to (modified in place). + name: Name of the new classical register. Defaults to ``"measurement"``. + + Returns: + The same *qc* instance with the register and measurements appended. + """ + cr = ClassicalRegister(qc.num_qubits, name=name) + qc.add_register(cr) + qc.measure(range(qc.num_qubits), cr) + return qc + + +def run_circuit(qc: QuantumCircuit, shots: int = 1024) -> tuple[dict, QuantumCircuit]: + """Simulate the circuit using Aer's SamplerV2 and return measurement counts. + + Adds a named classical register for measurements, simulates the circuit, + and extracts the measurement outcomes. + + Args: + qc: The quantum circuit to simulate. It will be modified in place to + add measurements. + shots: Number of simulation shots. Defaults to 1024. + + Returns: + Tuple containing: + - Measurement counts with bitstrings reversed to align qubit indices. + - The input circuit with measurements added. + """ + sampler = SamplerV2() + qc = measure_all_named(qc, "measurements") + job = sampler.run([qc], shots=shots) + result = job.result() + + # Grabbing only the desired outcomes + pub_result = result[0] + meas_bit_counts = pub_result.data.measurements.get_counts() # ty: ignore[unresolved-attribute] + + # get_counts() outputs reversed bitstrings, we just reverse them right back, + # so their indices align with the qubit indices + meas_bit_counts = {k[::-1]: v for k, v in meas_bit_counts.items()} + + return meas_bit_counts, qc + + +def compare_distributions( + qc1: QuantumCircuit, qc2: QuantumCircuit, counts1: dict, counts2: dict, code1: str = "None", code2: str = "None" +) -> float: + """Compute the Hellinger fidelity between two measurement distributions. + + If either code is 'steane' or 'shor', the corresponding counts are condensed + from physical qubits to logical qubits before comparison. + + Args: + qc1: The first quantum circuit. + qc2: The second quantum circuit. + counts1: Measurement counts from the first circuit. + counts2: Measurement counts from the second circuit. + code1: Error correction code for the first circuit ('steane', 'shor', + or 'None'). Defaults to 'None'. + code2: Error correction code for the second circuit ('steane', 'shor', + or 'None'). Defaults to 'None'. + + Returns: + Hellinger fidelity between the distributions (1 = identical, 0 = no overlap). + """ + if code1 in ["steane", "shor"]: + counts1 = condense_counts(qc1, counts1) + if code2 in ["steane", "shor"]: + counts2 = condense_counts(qc2, counts2) + + return hellinger_fidelity(counts1, counts2) + + +def parse_qubits(qc: qk.QuantumCircuit, physical_qubits: str) -> str: + """Extract logical qubit measurements from a physical measurement string. + + The circuit must use registers named 'qx' (where x is an integer) for each + logical qubit, with the decoded result stored in qx[0]. + + Args: + qc: The quantum circuit containing named registers. + physical_qubits: Measurement bitstring from physical qubits. + + Returns: + Logical measurement bitstring extracted from the named registers. + """ + # remove blanks caused by classical registers + physical_qubits = physical_qubits.replace(" ", "") + + # indices + def is_q_integer(s: str) -> bool: + """Checks if s is of form 'qx' where x in int (e.g. 'q1', 'q23').""" + return bool(fullmatch(r"q\d+", s)) + + data_indices = [qc.find_bit(register[0]).index for register in qc.qregs if is_q_integer(register.name)] + + # condensing + logical_qubits = "" + for index in data_indices: + logical_qubits += physical_qubits[index] + + return logical_qubits + + +def condense_counts(qc: qk.QuantumCircuit, counts: dict[str, int]) -> dict[str, int]: + """Map physical measurement counts to logical measurement counts. + + Requires the circuit to have decoded each logical qubit into the first + qubit of a register named 'qx', where x is an integer (e.g., 'q2'). + + Args: + qc: The quantum circuit with named registers. + counts: Dictionary mapping physical measurement bitstrings to counts. + + Returns: + Dictionary mapping logical measurement bitstrings to counts, where + multiple physical measurements may map to the same logical measurement. + """ + logical_counts = {} + for physical_measurement, count in counts.items(): + logical_measurement = parse_qubits(qc, physical_measurement) + logical_counts[logical_measurement] = logical_counts.get(logical_measurement, 0) + count + + return logical_counts diff --git a/uv.lock b/uv.lock index 7a1753c34..e3408e866 100644 --- a/uv.lock +++ b/uv.lock @@ -1589,11 +1589,13 @@ wheels = [ name = "mqt-bench" source = { editable = "." } dependencies = [ + { name = "mqt-qcec" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "qiskit", extra = ["qasm3-import"] }, + { name = "qiskit-aer" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] @@ -1625,6 +1627,10 @@ docs = [ { name = "sphinxcontrib-svg2pdfconverter" }, { name = "sphinxext-opengraph" }, ] +pytest = [ + { name = "mqt-qcec" }, + { name = "qiskit-aer" }, +] test = [ { name = "pytest" }, { name = "pytest-console-scripts" }, @@ -1635,6 +1641,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "mqt-qcec", specifier = ">=3.6.1" }, { name = "networkx", specifier = ">=2.8.8" }, { name = "numpy", specifier = ">=1.22" }, { name = "numpy", marker = "python_full_version >= '3.11'", specifier = ">=1.24" }, @@ -1642,6 +1649,7 @@ requires-dist = [ { name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1" }, { name = "numpy", marker = "python_full_version >= '3.14'", specifier = ">=2.3.2" }, { name = "qiskit", extras = ["qasm3-import"], specifier = ">=2.0.0" }, + { name = "qiskit-aer", specifier = ">=0.17.2" }, { name = "scikit-learn", specifier = ">=1.5.2" }, { name = "scikit-learn", marker = "python_full_version >= '3.14'", specifier = ">=1.7.2" }, ] @@ -1671,6 +1679,10 @@ docs = [ { name = "sphinxcontrib-svg2pdfconverter", specifier = ">=1.3.0" }, { name = "sphinxext-opengraph", specifier = ">=0.13.0" }, ] +pytest = [ + { name = "mqt-qcec", specifier = ">=3.6.1" }, + { name = "qiskit-aer", specifier = ">=0.17.2" }, +] test = [ { name = "pytest", specifier = ">=9.0.1" }, { name = "pytest-console-scripts", specifier = ">=1.4.1" }, @@ -1679,6 +1691,76 @@ test = [ { name = "pytest-xdist", specifier = ">=3.8.0" }, ] +[[package]] +name = "mqt-core" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/95/e1429f163a477845785a71d6dea92dfa1675d5228c1a01135f38340436ed/mqt_core-3.6.1.tar.gz", hash = "sha256:ce26f34bc0a363a795c8f95a2aa19341104b2961f3a2a4e2a19e2229e9e7dcd5", size = 655317, upload-time = "2026-05-20T00:34:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/e0/3e362a11e4103dd4c456f381daad92c5f18a6c467426999f58c480ba2cd6/mqt_core-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79de9549084ca6b107153b9fbab5987354a50c5ce692930b40679a4416f74c6c", size = 5180469, upload-time = "2026-05-20T00:33:52.936Z" }, + { url = "https://files.pythonhosted.org/packages/90/ea/2d5529b1d6983f5f20ba5e095a4a2476348c5faee92cbba847a69a8a48e9/mqt_core-3.6.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:a5e1c7f5f2522471700e908c8e2dc1963c1458e952a6814f021e0ed13724035d", size = 5671256, upload-time = "2026-05-20T00:33:55.019Z" }, + { url = "https://files.pythonhosted.org/packages/71/ac/1cb831f10d1be99114c8df214ff96bab3ffdc9913f4494984ce3398e5f5a/mqt_core-3.6.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87d93dd7a1f2a45083facc5e376b40e5c4306703eb5af56f9381678244226061", size = 6693517, upload-time = "2026-05-20T00:33:57.005Z" }, + { url = "https://files.pythonhosted.org/packages/b1/66/971c7c7e5f570899fbaed56b43523698faa0b46c575e9be114fd6a7c7eca/mqt_core-3.6.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:181b62397c04f30f4b482bfb588b1e08ea21d8ca6a78a94d45c8170d3dd0ade4", size = 7128865, upload-time = "2026-05-20T00:33:58.732Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/66776318b8399d3ee0e995cc5415b17e3b86d0debcc8f2b94054a35fd550/mqt_core-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1275e6be9f4198ffa16df06443df081ffa0d9016edd5bc22f2e5fb6866d692d1", size = 4046804, upload-time = "2026-05-20T00:34:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/b8/67/fdc2331352e35d938b276416008be7ffc0178eb66276b1445bc3a3767897/mqt_core-3.6.1-cp310-cp310-win_arm64.whl", hash = "sha256:74d03c9782cd2be1a7dc9f35a06815faf924caf9cf6572da815bfb59425f2c56", size = 7936592, upload-time = "2026-05-20T00:34:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/b0/a5a7450a9ffaff0ae8de49f65cd24c63901e3fee3dc740584c09b046165c/mqt_core-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27b80e793f27a963417cc06da908a31f51fae07722a20cdb5b32edd8e0276be1", size = 5181464, upload-time = "2026-05-20T00:34:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/9409c8f111c119722a8909bbfc92a7a3d8e4377e058ccfe62a46bf61ac09/mqt_core-3.6.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:2eae0ed0d4233749a8831bbeb32948da4031566acb742212b3681ed208289159", size = 5672582, upload-time = "2026-05-20T00:34:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/ba457c6db680a6e9aa84895bb25da826f03fb6454cfbd496a6c0ece3a9da/mqt_core-3.6.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bb184b52c318b3b9d5db97fa006120cbe683a8fa9cd80790bf24b67489cd204", size = 6694132, upload-time = "2026-05-20T00:34:07.64Z" }, + { url = "https://files.pythonhosted.org/packages/20/f9/3cc61de4fab97611c89d5977156ead60eb48c2721b8d4126d0ff1704e73c/mqt_core-3.6.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5c1b9750fa3e8447fd1bfd92b599688d3dc8574d3e70d1264c9a40b1b7be94", size = 7129376, upload-time = "2026-05-20T00:34:09.454Z" }, + { url = "https://files.pythonhosted.org/packages/59/b5/80713df591091312e3801a5ecb84bf93c2996dfb0dbd9a912973ed3adc9c/mqt_core-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d607b34f571500dbf0fb1ecb5f5ef95139dc32d57461a1cd0614387af778e75", size = 4047971, upload-time = "2026-05-20T00:34:11.097Z" }, + { url = "https://files.pythonhosted.org/packages/58/7e/e9ace3e69c3613f5e1b73e5497cc94c451c259ab66fc3fda077f908ffc32/mqt_core-3.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:8520f7cfa3b5e7465555fe06c3c1c17c194cfca92a06ac3f1bdf3c7660c1ef88", size = 7937030, upload-time = "2026-05-20T00:34:12.761Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/423abe9632f79a8174d87b0b3f60d08d8e033e5c947c1a35e8cf7f4b4b33/mqt_core-3.6.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:8136975d84f3ad721a01e4461dbd15e8ae00630e8cf8ca000e247c68d44b32cc", size = 5177324, upload-time = "2026-05-20T00:34:14.518Z" }, + { url = "https://files.pythonhosted.org/packages/8f/45/c2af228132100238121b256c84172acaaba3c4f68b45b0fcfab2a7684910/mqt_core-3.6.1-cp312-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ee0433a5f4a22e6542e86558e29665219415aad730f699c555743d8fbd38a110", size = 5668253, upload-time = "2026-05-20T00:34:16.491Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c1/1e4596f76d52e56b96849575795be5132320c7fa39df1dd83e1eff9f7d4a/mqt_core-3.6.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7ff7726627bfe4e779fa74397b89d9e686543a99ea2132d588aee7da6ecc89d", size = 6679218, upload-time = "2026-05-20T00:34:18.481Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/de19e1e6f9f2b0cfdca08af28ff033a883a1995185724de8812728d45f3f/mqt_core-3.6.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cf804ce909a7f9301759cfbc83b0d7dd6234752504f24185a39f616078bfb", size = 7112143, upload-time = "2026-05-20T00:34:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e4/8d18330770afe008a5f26598e5b8025404d3adab04d8fe3e590576fd31aa/mqt_core-3.6.1-cp312-abi3-win_amd64.whl", hash = "sha256:ef594511df7596c23c61d96adf8d3f805ee81aa1afb52fc589d19b2b036d88fc", size = 4040427, upload-time = "2026-05-20T00:34:22.237Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1c/5f054d1c0a1f3be73135bcb324ff5c1ef1312fa2d94c78a88b7e264dec0d/mqt_core-3.6.1-cp312-abi3-win_arm64.whl", hash = "sha256:9ed5a594426937520bc2b3c8f92a5a52cb37df8a9d55661e1a6d8ec37d169b39", size = 7929919, upload-time = "2026-05-20T00:34:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/3f/12/f50cadbb52bda38a4d727a90d5542c64078da8b5f0f18f36938e41877bd1/mqt_core-3.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c103a008405f4b554ee7bf092cd26a7d46cf7425c5ca8fa9b0a2e0004c4d8fa9", size = 5190293, upload-time = "2026-05-20T00:34:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0f/53b37b867d077124e30907bc520a80ceab0ca045f7687ea3aac301eebbab/mqt_core-3.6.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:0ef1c62495ed4b110f3bce89dcf0a21a1353f64010cccc1ec71a50ce0cdea26e", size = 5682141, upload-time = "2026-05-20T00:34:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ce/3bb8f5f64897fc1d70d637097098c1ada7abf579a93f8d66e392a3eb5e18/mqt_core-3.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7459aae9b2d80be7b32e3317d3fb86f5f5977b27c149de1e6d68c67d4c80b71", size = 6699266, upload-time = "2026-05-20T00:34:29.069Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9f/87088625f8d5426caf4a66bcc288e1833999c5dd3e9718604117821b25ec/mqt_core-3.6.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e4183e8ec739087e5f94319bfb054318b184d6fdaab2bca5207745ff89dae09", size = 7129965, upload-time = "2026-05-20T00:34:31.213Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b7/4b9b70f914add8961fb736008618ea28f93b5acdba19878fde7d9fd4f30a/mqt_core-3.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c7a2bdc0ff010672501cfc5735a547d9b15cab4f1604a78c687b69924ab25f", size = 4133733, upload-time = "2026-05-20T00:34:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5d/4e1e3e2ea7cab9d093a865c0e2b3de0729257e4eaff8abcd654503e43aa7/mqt_core-3.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:31df5b87f5afc09bbe3f815df434f0306a56b952b4f5dc2599e4d53625031b09", size = 8008654, upload-time = "2026-05-20T00:34:34.446Z" }, +] + +[[package]] +name = "mqt-qcec" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mqt-core" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/16/1e900e1c89a034ee8d6196de3d2b5f30711ffe159c5a4de0fdc6189374b3/mqt_qcec-3.6.1.tar.gz", hash = "sha256:0d053387fa2d660fcf8ec9ab6e5da6069634d73a3e95a7a9e5b5fed7479af3e3", size = 293070, upload-time = "2026-05-20T22:19:36.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e8/c3718edaf04407b42d4476cadeb08d55c45b8693cc89da74fb4050157c09/mqt_qcec-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1623a60eb35f558db3e6bc38345d8ce6fcd165e48817f6227dc53cf064f19b1", size = 224147, upload-time = "2026-05-20T22:18:55.334Z" }, + { url = "https://files.pythonhosted.org/packages/d1/55/6e50ca989af1484118e88aa0b80208ad83d4f8b6bec9c19bbb344dce5e62/mqt_qcec-3.6.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:93d51dd278966aa222a4c2d867d8f61b7a40638160398cc2d09e97bdf1dab94a", size = 237244, upload-time = "2026-05-20T22:18:57.565Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a9/4c61d6af5acb56ef0e1b69d650bb774e2383b7355b0071f1f1141cb31214/mqt_qcec-3.6.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ca76885b3afc20460a38a3d7a4c08c4e51dd35d7fc38f12870595f312a318fb", size = 238734, upload-time = "2026-05-20T22:18:59.599Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/d91036cb97f10b2276cf0a192a0a619e2ac82fa6f54cfcf354c85509677d/mqt_qcec-3.6.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79018893072853030faaa3e066981c6e7b20fc2ca7cd6c5820ef997c65350ffb", size = 249180, upload-time = "2026-05-20T22:19:01.135Z" }, + { url = "https://files.pythonhosted.org/packages/d4/85/8775d220d06e02ea701d2f6994dc17be173013ad4d9eacd7435cafae1f8a/mqt_qcec-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:2e1db239d0bec8198d1317c8b1cf08ed20c887eb63bae85590035655c8e6b623", size = 448206, upload-time = "2026-05-20T22:19:02.659Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/82510c8721b63891e72e7ad75b70f7918e42f2a7b8a2ac5ab960939a4cc3/mqt_qcec-3.6.1-cp310-cp310-win_arm64.whl", hash = "sha256:c093175c97a67c5731bbadf7cd7063abda7e1bd50ea3e80f821a9ac0b2e65095", size = 614084, upload-time = "2026-05-20T22:19:04.309Z" }, + { url = "https://files.pythonhosted.org/packages/de/0b/0619ba807fd52ab1edefb22f352d6b70e147d21053cf8fc5d3770c125e95/mqt_qcec-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:12a0640b2480f72292e75a57783403edf811650405c155a860083c601f381961", size = 224721, upload-time = "2026-05-20T22:19:05.916Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/14d46d1ddba1aaa26136ef639028c3905488be5b83134f67241689d5cf07/mqt_qcec-3.6.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:88e36543d8ea65886360960814416c27b6f8474e255d9f4cb2254c553546dd7c", size = 237716, upload-time = "2026-05-20T22:19:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/ff4896dd651786d67fd5b6b8a3ee3ddaf384b1b9b0c60b75284c119aee88/mqt_qcec-3.6.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dc2b0fa06e5484328f1f0bba6097cb683ddc3f99b6a545aea775585a027d66a", size = 239552, upload-time = "2026-05-20T22:19:09.017Z" }, + { url = "https://files.pythonhosted.org/packages/87/5e/2d58b085e09b6d8afe4455546c5d2f0e2780f921999a5d55240edb0cf3b0/mqt_qcec-3.6.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9a3bc83ef743f3365daf807f3f4618d47aa856ebdd3ac4fb7687efc6f976641", size = 250637, upload-time = "2026-05-20T22:19:10.87Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7f/3268ce58888314697d6f5c6e9aa29ca238c7649bdaf8844a4cd7cca313e5/mqt_qcec-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:b52795a6afbda923823da3485ee584a56c7fb4a4fa7c6b977091844021f6369e", size = 448256, upload-time = "2026-05-20T22:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7a/65ac8b6a801eaa1c5c6112cc3b6158b2d8b1fc8d23e67f3a1ead3dedb7b2/mqt_qcec-3.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:336dcdd229827b07cb0544fb6bcc1bb2ac38817579d2d8a231bd2f9262261bf6", size = 614236, upload-time = "2026-05-20T22:19:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/01/4c/f058c0f6acf45342c9713095aa23275cc591bad35ba5c21acec3203548a7/mqt_qcec-3.6.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:8300e38baaeaade42e278cc89d472f0442511b6380ff9e0366ae68993ea0e0b0", size = 222543, upload-time = "2026-05-20T22:19:15.753Z" }, + { url = "https://files.pythonhosted.org/packages/56/b8/705803cf605a4f30987031da56d3136df1be995edff4558251776b282169/mqt_qcec-3.6.1-cp312-abi3-macosx_11_0_x86_64.whl", hash = "sha256:1a649008da91b42b7a7231512ddfe78b851454bab4e6d062c29049ade5d16ec1", size = 235672, upload-time = "2026-05-20T22:19:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/30/3c/c55bfc33eea06e713077cc3162e2e2f2a3a836e8ab1f13798a2864759795/mqt_qcec-3.6.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03e37a3ee86ace7d8541fee0b647feb004465e2c56624a3337cdc7a77c0c6c75", size = 235953, upload-time = "2026-05-20T22:19:19.995Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/a2562098c399c1cafe48e03f8523a08c149ed8104d382d4e24cfba614d99/mqt_qcec-3.6.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529835521e8e4e217cdd61215b8a7c47a349baa86b1f32408c19cb4fb0399a54", size = 246929, upload-time = "2026-05-20T22:19:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f2/a17519cfab01176c03a96079c2a712ef2a2c99833abc6d97c7b9aea46969/mqt_qcec-3.6.1-cp312-abi3-win_amd64.whl", hash = "sha256:5ee3a48fbc3b145a92d9209bebaf92d01741f481789fdd5935cc840fa9029d40", size = 445481, upload-time = "2026-05-20T22:19:23.092Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c8/2710ed3781bcb80b7c87451b78e081bdb31570820031eb13396623853a49/mqt_qcec-3.6.1-cp312-abi3-win_arm64.whl", hash = "sha256:c4f1068fd2ae8d926ba9048e99306b3349e06f7509a40454f55dc786c7665055", size = 611543, upload-time = "2026-05-20T22:19:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9d/a2303a4bab2b1a35c58248185f89e4a5871ec14c088e9e84b7bbc80bca73/mqt_qcec-3.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e888f31dacd78c27005c8e4eca45674b3295087717d47b542e62ce18c12d9a31", size = 226598, upload-time = "2026-05-20T22:19:26.828Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/faeb4ec285f47dea7463b8b859ea9e8c7b9ed78481584dfe538e83f86eb4/mqt_qcec-3.6.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1d8800075dd71c125c22cb3b0b3502b5058e7cd83964e7c68b05ec9eb058421f", size = 240324, upload-time = "2026-05-20T22:19:28.427Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/3654183129053163f67b97e08ea4547947e4b30593be3f0e3c4c65510596/mqt_qcec-3.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dd36f4b68ef86d42cbe140e48922df018a47d73000b44aae9cc9fe66217632c", size = 239324, upload-time = "2026-05-20T22:19:29.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/26/ec188dde64eab22f28d6555bf4d14cb4af249f81fc710888f68f618bd720/mqt_qcec-3.6.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cedf1a5ad487622af7edcc2f7c2af6e3ede791130bd9cd88d333fa9a5a99a0c5", size = 250367, upload-time = "2026-05-20T22:19:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/db/34/02404e95f3b905689849eb797372e31f8714ab5de5a28fd3f2f695dbd20f/mqt_qcec-3.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb63f16760c333c3d04a56970c1bdfe21e99d0da9b9b039b0085813195e6ee7f", size = 465962, upload-time = "2026-05-20T22:19:33.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/dd/3ac789c6d6e835c7c483422db15c65f566e1357ba988f2161685adb9413e/mqt_qcec-3.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a20662bb0e0e0d067c970fc6f89e7c74edb2fc16e054d53c615eff0746f41a6", size = 637086, upload-time = "2026-05-20T22:19:34.778Z" }, +] + [[package]] name = "myst-nb" version = "1.4.0" @@ -2203,7 +2285,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2759,6 +2841,58 @@ visualization = [ { name = "sympy" }, ] +[[package]] +name = "qiskit-aer" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "psutil" }, + { name = "python-dateutil" }, + { name = "qiskit" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/6c/6b8b35f67159401580665c59ae64d676bef9e85aac4d2a50831cbe32f652/qiskit_aer-0.17.2.tar.gz", hash = "sha256:134eef8e509311955a15be543d2ba368f988f3583a2bc1f548af3196da820eb4", size = 6551618, upload-time = "2025-09-17T13:55:25.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/7b/2d2a5477f48e920b5b12eced57b386ba8a784a27a7495da095fb00e64f1e/qiskit_aer-0.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4cef22b09c6d7f1afa4886fe8ff14f54ccfe830c50f322182f06f5f8a72b48d3", size = 2505354, upload-time = "2025-09-17T13:53:56.779Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68b3b84d41dc52d1c7441ec2fb402d8274dfcc5c76c191d782f6457aa9c6/qiskit_aer-0.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99b92f70d669e3f869bc341ed4b3b1943e8a7c85643f1d289db8b08e0e4397a4", size = 2115952, upload-time = "2025-09-17T13:53:59.107Z" }, + { url = "https://files.pythonhosted.org/packages/e0/69/df28af389bb2cb4c39ce8a36170600ff23ab09b5a7ce1c11a3a102bfc99b/qiskit_aer-0.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f149e5921f54cbefc220021b6144a197cbc132474a7ea55e36e5b7909ab67a", size = 6454168, upload-time = "2025-09-17T15:22:20.03Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f7/c25bcb06d66e45914da7e80f7a0a15e390e97ce80531c9924b67ae9d1fd2/qiskit_aer-0.17.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a84a59750faa83186373bd32af3af43d6303c6469899aa9018031e6fa296754", size = 7974706, upload-time = "2025-09-17T13:54:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/c16875bb584ce1dea37a232a1b4ab0f3b21ddea54ae9b6b5c36397c7d513/qiskit_aer-0.17.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:909213047102b591ab04cf4bb7d4f0a6aa56d38cee331c8a29060d1174be536d", size = 7926320, upload-time = "2025-09-17T14:51:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/62/1d/4aabb58556833100f52d39139d06a494555cfb0dad04e2f515c3a62a9455/qiskit_aer-0.17.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32b2b056b1132f21af3ec76ec4de19b1b1871aa842a8fa3f01617a88f929365c", size = 12381501, upload-time = "2025-09-17T13:54:02.37Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1e/ae00ed91d4b1aa0776cf85fa1db371856037fd5c0ae6309339e42b54fa7a/qiskit_aer-0.17.2-cp310-cp310-win32.whl", hash = "sha256:ddc7360317d652ed7fbc12a83f3b3adf28258433b55f2c6022c1448473fdb407", size = 6920674, upload-time = "2025-09-17T13:54:04.823Z" }, + { url = "https://files.pythonhosted.org/packages/2f/7c/6db320f9f41adce182ba4359bc4066255b43a08059b3b841dc8807f09b8e/qiskit_aer-0.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:38b2c99d7af65716f6c48616a57ffa79ec827a8fc70257f925f6904c7b1a1a0f", size = 9561666, upload-time = "2025-09-17T13:54:06.461Z" }, + { url = "https://files.pythonhosted.org/packages/71/7f/5e687162d9e0c25898a1d964a759e773f37a6921abbac0eca14c9ec9ae21/qiskit_aer-0.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8723a61aa3925508a977dd92ae135f2aee465cf74bb9ed7c75b5cc98628e4b3", size = 2506674, upload-time = "2025-09-17T13:54:08.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/54/be4d6ceaa305155fac89892a8d7dd6f01eed5f161bfceb7862b4e4a853cd/qiskit_aer-0.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bdf5aacaf27576de988dc1aea915fdcb18c83a9732de2278137004fc076470", size = 2116548, upload-time = "2025-09-17T13:54:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/70/63/b79fc699f5e892fcb61c4ac474b7cedcce0688dc57d1738b9c8053c23db1/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecd74ca2ce45bbc673d5f7d2e3fdc5218dc7d05a9d24feaa84d48cf23ca028d", size = 6458841, upload-time = "2025-09-17T15:22:22.668Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d7/3c2bb19b0f854fbcc6253b94632fd3feec41873c2b08b3f482ced8f54dc7/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e5df2f2aa8cf189639df870303543c3febb3a282113c91bc27a8c7c4624d225", size = 7972597, upload-time = "2025-09-17T13:54:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/75/ea/4c4b20415090f69a97fe8dc5e1e09549f13c88f5b523700855d4d130d65a/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1274f5609269fc9762835d978d9e8110ca84af2d0d1dc9b08552a34a6068520", size = 7926625, upload-time = "2025-09-17T14:51:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/9e/88/f5b350f60ecefdc1ef5794ec9524dee685e3d32b8532f2142bb1afa39d32/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bd91f682dc62c0e62c30651daf45aebd462bdbc2b1a2d028be3cc85b4b09eed", size = 12374395, upload-time = "2025-09-17T13:54:13.362Z" }, + { url = "https://files.pythonhosted.org/packages/a7/96/3f76b86e5165ab935228e1488beb9f801c3db484d915366e9ae36ea8d8ae/qiskit_aer-0.17.2-cp311-cp311-win32.whl", hash = "sha256:7ea01d85d9d6a4cddd205ed118075401323517b69624f59f5607f8a14b72fef9", size = 6921401, upload-time = "2025-09-17T13:54:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/ac0a55a6fb539355e9e629f42f03032f9e93acce2390a87a7ab8c56764f7/qiskit_aer-0.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba8ee895803d618cf1cc13f948c5806e52c27d673d828e196f57774649b05cdf", size = 9562419, upload-time = "2025-09-17T13:54:17.445Z" }, + { url = "https://files.pythonhosted.org/packages/0a/2c/7039b1891377ef081c92af79cee230be0a01e52c21561469f6807f17fe96/qiskit_aer-0.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5f03bf3f45f7f6cc6e480df473e4750178cb66ee7fb44d85d98c13901e81c42c", size = 2508338, upload-time = "2025-09-17T13:54:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/f1d6906c2cb3ff3ec94f97656abbc56b80b80c4677ff603ee40063cf9c84/qiskit_aer-0.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:abb03d621cfd30e608ba8ddbb9e673707e6f790a744130d1d6355e3e10554d13", size = 2117032, upload-time = "2025-09-17T13:54:21.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/b3/86a9687b2123201badcca23c0954fe17e83d648cfb1737558c00783e3ea6/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3ee2debad4dd9d1ff021002e82363a83ee22b1440ccbc293a444072c9fd0c1", size = 6454082, upload-time = "2025-09-17T15:22:24.811Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/45a3d07b0372317f33ce3abaa438d668b57f3ecdd0c62dcb4c2d43e44d17/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:782f6ba0bdd08faec19f7bbb65e95fc70b0c2d097b056fc929a95c084b57c203", size = 7974594, upload-time = "2025-09-17T13:54:22.628Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c3/91ea504db5ba2c43f1fc5918ff60098aa730a5db40830a096855325b2b66/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e91fc4f0a26540ff0e9d0a30b8be3e0f12b4c9c59ec1afffb753944d47e1888", size = 7926812, upload-time = "2025-09-17T14:51:09.356Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/c47b356b90dd00b9b19fdcaa8f6776613db0885630f7095f92659f62b5c8/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8857aad723036ff818af14bbd4c3375559741366bffce7b36b4eeb306b88cf", size = 12375640, upload-time = "2025-09-17T13:54:24.582Z" }, + { url = "https://files.pythonhosted.org/packages/06/eb/8b796a34622392ee1f66b7d03ba31385ef559cd3a145ec6de2556ebb983e/qiskit_aer-0.17.2-cp312-cp312-win32.whl", hash = "sha256:a9abdb24318c417b69867c6d43aed4684b67320b1e7010f4c57c84fdeff89a13", size = 6922323, upload-time = "2025-09-17T13:54:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/30/f7/5943ba7f6be0a02667593ef5f684359a62d5a46ba9dac9a0367c3ab3d1d8/qiskit_aer-0.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:80c419bb3fb65a5135286ce4e98abd68b9dc836b77affbbc4b06721d53ce1e3c", size = 9563069, upload-time = "2025-09-17T13:54:28.885Z" }, + { url = "https://files.pythonhosted.org/packages/1e/96/0b7f3f7ee5cfc9dde495a2e324e53432abbfccb95a62cafa09e4fc07706d/qiskit_aer-0.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a8ac09544489e34cb60bc0e615bc5ae725de581c9a6bf5cd17b57f2a7baf9f16", size = 2508547, upload-time = "2025-09-17T13:54:32.728Z" }, + { url = "https://files.pythonhosted.org/packages/d7/08/4adfd24bd337d1b1b45a0fd85a2d0f1b9b386dfc9db8135fe5abbad3a0fc/qiskit_aer-0.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c1bf5072bc54250009751350aaaed06bef15ada7ab43e6ce2c934831f6ee6ea", size = 2117037, upload-time = "2025-09-17T13:54:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/93/3a/6068244629b8f04ce48fa4dddb8d0874f28e91611d90571e69d9417f22ba/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd9d3c5e6c5d09cb0a821bf5077a14e7f5f8db5c3d700023be92ce6a05923309", size = 6454589, upload-time = "2025-09-17T15:22:26.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/68/27ddd833d700bc9f9adede6e281146c0229acce895e64dcd32f1b62c90e9/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46820fb38bf85f8c6f8aaa350dd90d01b0be10d16f5f1ef4188882b3a0531f38", size = 7977976, upload-time = "2025-09-17T13:54:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/a1cd95daf75d09d2dc1744cde95d5d18fed61a0e4922788fb916a7cd8152/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2749e6027f67e1f6b9d328d2dda2d4bf926aebd3653edc62e94c45d8237294d8", size = 12376191, upload-time = "2025-09-17T13:54:38.092Z" }, + { url = "https://files.pythonhosted.org/packages/d6/69/e2f979e2fca054b0092fc52c46da050298513b9b03531305bc3f340c7669/qiskit_aer-0.17.2-cp313-cp313-win32.whl", hash = "sha256:c3ffd40a64bfcf8a6d10cbfdca8734d49ec57502fd70dc63aae9ed3819249dd6", size = 6922275, upload-time = "2025-09-17T13:54:40.024Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/195cb69d3af4359544939378879093764bd35d8abd7ac0de840bb5477d27/qiskit_aer-0.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:b38c5dfdc6cb2bacac78a47b0df8247123051564007fdecedb8ffbd4256f0f09", size = 9563116, upload-time = "2025-09-17T13:54:42.061Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1b/b0516cd3d0e83ebe30e8d04d2a156dac883fd8ac4521deb12de582b2fc78/qiskit_aer-0.17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c282b9b65f2b011d740e76b0ab44201c70d8d48894ddc12e22442acbb81cc7eb", size = 2393891, upload-time = "2026-02-04T21:30:47.913Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/c8bf7942ca12d50c4c8c9fde82f25ebcd6198b98f66c51616a9225d541bc/qiskit_aer-0.17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:193de16895ee989a5259331d0f1b1dcad606d506e5e831683ff165e82851a7ac", size = 2117650, upload-time = "2026-02-04T21:30:55.486Z" }, + { url = "https://files.pythonhosted.org/packages/ff/27/f7b518f0928792e454ca9018d712769abf96033902e41bb3b648ff14833b/qiskit_aer-0.17.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b84564d563fb06adb454f3528dcc343be327c30cbd5f9f40aec7efd21d241e55", size = 14160447, upload-time = "2026-02-04T21:30:57.829Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c1/59fe9c10e8d53533990ccf1bdf87ac74f77ced57d63a233f759df02bd361/qiskit_aer-0.17.2-cp314-cp314-win_amd64.whl", hash = "sha256:5d7b22dd945df4c69d57e966efb549cf9186055e5f32c649a91b7dd1eb133f07", size = 9697912, upload-time = "2026-02-04T21:30:59.913Z" }, +] + [[package]] name = "qiskit-qasm3-import" version = "0.6.0"