diff --git a/CHANGELOG.md b/CHANGELOG.md index 87ab2db5d..e85ad4e3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ that minor releases may include breaking changes. - ✨ Add quantum teleportation benchmark ([#1045]) ([**@AsharHusain**]) - ✨ Add ML-QAE benchmark ([#1044]) ([**@algovista-collab**]) - ✨ Add Superdense Coding benchmark ([#1037]) ([**@Anshu666666**]) +- ✨ Add `encoding` option for Steane's and Shor's codes ([#930]) + ([**@Felix-Gundlach**], [**@emilkanic0909**], [**@SalehAlsherif**]) ### Changed @@ -345,6 +347,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [#997]: https://github.com/munich-quantum-toolkit/bench/pull/997 [#952]: https://github.com/munich-quantum-toolkit/bench/pull/952 [#946]: https://github.com/munich-quantum-toolkit/bench/pull/946 +[#930]: https://github.com/munich-quantum-toolkit/bench/pull/930 [#925]: https://github.com/munich-quantum-toolkit/bench/pull/925 [#914]: https://github.com/munich-quantum-toolkit/bench/pull/914 [#895]: https://github.com/munich-quantum-toolkit/bench/pull/895 @@ -401,6 +404,9 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [**@TomasVF**]: https://github.com/TomasVF [**@flowerthrower**]: https://github.com/flowerthrower [**@johanneswittmann9**]: https://github.com/johanneswittmann9 +[**@Felix-Gundlach**]: https://github.com/Felix-Gundlach +[**@emilkanic0909**]: https://github.com/emilkanic0909 +[**@SalehAlsherif**]: https://github.com/SalehAlsherif [**@cnYui**]: https://github.com/cnYui [**@algovista-collab**]: https://github.com/algovista-collab [**@Anshu666666**]: https://github.com/Anshu666666 diff --git a/docs/parameter.md b/docs/parameter.md index 11eb96531..4176bec94 100644 --- a/docs/parameter.md +++ b/docs/parameter.md @@ -64,6 +64,21 @@ print(get_available_device_names()) the mirror. Circuits that Qiskit cannot invert, for example those with mid-circuit measurements, resets, or unsupported control flow, raise `ValueError`. Use `False` to generate these circuits without a mirror. +- `encoding`: Error-correcting code applied to the circuit. Only the ALG level + supports it; other levels ignore it with a warning. Gates without a + transversal realization become placeholders: opaque logical gadgets named + `_ideal_logical_`, for example `steane_ideal_logical_t`. OpenQASM + 3 export does not support placeholders, so save encoded circuits as QPY. + Rotations are approximated, so gate counts can differ between Qiskit versions. + +Possible values for `encoding`: + +```{code-cell} ipython3 +:tags: [hide-input] +from mqt.bench.error_correction import get_available_encoding_names + +print(get_available_encoding_names()) +``` ## Native Gate-Set Support diff --git a/src/mqt/bench/benchmark_generation.py b/src/mqt/bench/benchmark_generation.py index 1ca47ab48..0bcdb952c 100644 --- a/src/mqt/bench/benchmark_generation.py +++ b/src/mqt/bench/benchmark_generation.py @@ -11,6 +11,7 @@ from __future__ import annotations import json +import warnings from copy import deepcopy from enum import Enum, auto from typing import TYPE_CHECKING, Unpack, assert_never, overload @@ -23,6 +24,7 @@ from qiskit.transpiler.passes import OptimizeCliffordT, RemoveFinalMeasurements from .benchmarks import create_circuit +from .error_correction import get_available_encoding_names, get_transpiler from .targets.gatesets import get_target_for_gateset from .targets.gatesets._compilation import prepare_target from .targets.gatesets.ibm import configure_fractional_angles @@ -117,6 +119,7 @@ def _record_generation( target: Target | None = None, opt_level: int | None = None, mirror: bool = False, + encoding: str = "", ) -> None: """Attach the compilation settings to the circuit's generation record.""" generation.update({ @@ -124,6 +127,7 @@ def _record_generation( "opt_level": opt_level, "target": target.description if target is not None else None, "mirror": mirror or generation.get("mirror", False), + "encoding": encoding or generation.get("encoding"), }) qc.metadata = (qc.metadata or {}) | {"mqt_bench_generation": generation} @@ -239,6 +243,7 @@ def get_benchmark_alg( benchmark: str, circuit_size: int, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -250,6 +255,7 @@ def get_benchmark_alg( benchmark: QuantumCircuit, circuit_size: None = None, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -261,6 +267,7 @@ def get_benchmark_alg( benchmark: str | QuantumCircuit, circuit_size: int | None = None, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -271,6 +278,7 @@ def get_benchmark_alg( benchmark: str | QuantumCircuit, circuit_size: int | None = None, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -280,6 +288,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: Name of an error-correcting code from `get_available_encoding_names()`, or "" for none. 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. @@ -287,12 +296,22 @@ def get_benchmark_alg( Returns: Qiskit::QuantumCircuit representing the raw benchmark circuit without any hardware-specific compilation or mapping. """ + valid_encodings = {"", *get_available_encoding_names()} + if encoding not in valid_encodings: + msg = f"Invalid `encoding` '{encoding}'. Must be one of {valid_encodings}." + raise ValueError(msg) + qc, generation = _get_circuit(benchmark, circuit_size, random_parameters, **kwargs) if qc is benchmark: qc = qc.copy() - _record_generation(qc, BenchmarkLevel.ALG, generation, mirror=generate_mirror_circuit) + _record_generation(qc, BenchmarkLevel.ALG, generation, mirror=generate_mirror_circuit, encoding=encoding) if generate_mirror_circuit: - return _create_mirror_circuit(qc, inplace=True) + qc = _create_mirror_circuit(qc, inplace=True) + + if encoding: + # The encoded circuit keeps the metadata, including the generation record. + qc = get_transpiler(encoding)(qc).transpile() + return qc @@ -581,6 +600,7 @@ def get_benchmark( target: Target | None = None, opt_level: int = 2, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -595,6 +615,7 @@ def get_benchmark( target: Target | None = None, opt_level: int = 2, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -609,6 +630,7 @@ def get_benchmark( target: Target | None = None, opt_level: int = 2, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -622,6 +644,7 @@ def get_benchmark( target: Target | None = None, opt_level: int = 2, *, + encoding: str = "", generate_mirror_circuit: bool = False, random_parameters: bool = True, **kwargs: Unpack[ConfigurationOptions], @@ -635,6 +658,8 @@ 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: Name of an error-correcting code from `get_available_encoding_names()`, or "" for none. + Only supported for `BenchmarkLevel.ALG`; ignored (with a warning) for other levels. 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. @@ -642,12 +667,20 @@ def get_benchmark( Returns: Qiskit::QuantumCircuit object representing the benchmark with the selected options """ + if level is not BenchmarkLevel.ALG and encoding: + warnings.warn( + f"`encoding` is only supported for BenchmarkLevel.ALG and will be ignored for level={level.name}.", + UserWarning, + stacklevel=2, + ) + if level is BenchmarkLevel.ALG: return get_benchmark_alg( benchmark=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 1f94acad9..1eadc730f 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 Z-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 66f618e39..40853bfbb 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, ) diff --git a/src/mqt/bench/components/__init__.py b/src/mqt/bench/components/__init__.py new file mode 100644 index 000000000..1b6eef3a4 --- /dev/null +++ b/src/mqt/bench/components/__init__.py @@ -0,0 +1,11 @@ +# 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 + +"""Reusable circuit components for quantum error-correcting codes.""" + +from __future__ import annotations 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..3bd9bfd1c --- /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. + + Args: + 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. + + Args: + 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..2667a0039 --- /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. + + Args: + 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/__init__.py b/src/mqt/bench/error_correction/__init__.py new file mode 100644 index 000000000..1355b43fc --- /dev/null +++ b/src/mqt/bench/error_correction/__init__.py @@ -0,0 +1,51 @@ +# 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 + +"""Initialization of the error correction module.""" + +# ruff: file-ignore[non-empty-init-module] + +from __future__ import annotations + +from .ec_transpiler import ECTranspiler, LogicalQubit +from .shor_transpiler import ShorTranspiler +from .steane_transpiler import SteaneTranspiler + +__all__ = [ + "ECTranspiler", + "LogicalQubit", + "ShorTranspiler", + "SteaneTranspiler", + "get_available_encoding_names", + "get_transpiler", +] + + +_TRANSPILERS: dict[str, type[ECTranspiler]] = {cls.CODE_NAME: cls for cls in (ShorTranspiler, SteaneTranspiler)} + + +def get_available_encoding_names() -> list[str]: + """Return the names of all supported error-correcting codes, i.e. the valid ``encoding`` values.""" + return sorted(_TRANSPILERS) + + +def get_transpiler(encoding: str) -> type[ECTranspiler]: + """Return the transpiler class for the given error-correcting code. + + Arguments: + encoding: Name of the error-correcting code (see :func:`get_available_encoding_names`). + + Raises: + ValueError: If ``encoding`` is not a supported error-correcting code. + """ + if encoding not in _TRANSPILERS: + msg = ( + f"'{encoding}' is not a supported error-correcting code. Available codes: {get_available_encoding_names()}" + ) + raise ValueError(msg) + return _TRANSPILERS[encoding] diff --git a/src/mqt/bench/error_correction/ec_transpiler.py b/src/mqt/bench/error_correction/ec_transpiler.py new file mode 100644 index 000000000..ce35457d0 --- /dev/null +++ b/src/mqt/bench/error_correction/ec_transpiler.py @@ -0,0 +1,276 @@ +# 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 + +"""Base class for transpilers that encode circuits with a quantum error-correcting code.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from copy import deepcopy +from dataclasses import dataclass +from typing import ClassVar + +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile +from qiskit.circuit import AncillaRegister, Gate, Instruction + + +@dataclass +class LogicalQubit: + """Encapsulates the physical registers representing a single encoded logical qubit.""" + + data: QuantumRegister + bit_flip_syndrome: AncillaRegister + phase_flip_syndrome: AncillaRegister + bit_flip_measure: ClassicalRegister + phase_flip_measure: ClassicalRegister + + def get_all_registers(self) -> list[QuantumRegister | ClassicalRegister]: + """Return all active registers for this logical qubit.""" + regs: list[QuantumRegister | ClassicalRegister] = [self.data] + if self.bit_flip_syndrome is not None: + regs.extend( + reg + for reg in [ + self.bit_flip_syndrome, + self.phase_flip_syndrome, + self.bit_flip_measure, + self.phase_flip_measure, + ] + if reg is not None + ) + return regs + + +class ECTranspiler(ABC): + """Encodes a QuantumCircuit with a quantum error-correcting code. + + Subclasses declare the code's parameters as class attributes and implement the abstract + methods. + """ + + #: Short name of the code, used to name the encoded circuit and its placeholders. + CODE_NAME: ClassVar[str] + #: Number of physical qubits used to encode a single logical qubit. + BLOCK_SIZE: ClassVar[int] + #: Number of ancilla qubits (and classical bits) used per bit-flip syndrome extraction. + BIT_FLIP_SYNDROME_SIZE: ClassVar[int] + #: Number of ancilla qubits (and classical bits) used per phase-flip syndrome extraction. + PHASE_FLIP_SYNDROME_SIZE: ClassVar[int] + #: Basis gate set the original circuit is transpiled to before logical replacement. + TARGET_GATE_SET: ClassVar[list[str]] + #: Transversal gates: each one is realized by applying one physical gate at every physical + #: qubit position of the involved data registers. Implement other gates as + #: ``_logical_`` methods on the subclass. + TRANSVERSAL_GATES: ClassVar[dict[str, Gate]] = {} + + def __init__(self, original_circuit: QuantumCircuit) -> None: + """Initialize the transpiler with the original QuantumCircuit. + + Args: + original_circuit: Original circuit to transpile using the error-correcting code. + """ + self.original_qc = original_circuit + self.num_logical_qubits = original_circuit.num_qubits + self.logical_qubits: list[LogicalQubit] = [] + self.transpiled_qc = QuantumCircuit() + self.encoded_qubits = [False for _ in range(self.num_logical_qubits)] + + def transpile(self) -> QuantumCircuit: + """Encode the original circuit with the error-correcting code. + + The original circuit is first decomposed into the code's gate set, and rotations are + approximated with Clifford+T sequences. The gate counts of the encoded circuit can + therefore differ between Qiskit versions. + + Returns: + The encoded circuit. It keeps a copy of the original circuit's metadata. + """ + # reset before each transpilation + self.logical_qubits: list[LogicalQubit] = [] + self.transpiled_qc = QuantumCircuit() + self.encoded_qubits = [False for _ in range(self.num_logical_qubits)] + + for instruction in self.original_qc.data: + if instruction.is_control_flow(): + msg = f"Error-correction transpilation currently does not support control-flow operations such as {instruction.operation.name}." + raise ValueError(msg) + + self.original_qc = transpile( + self.original_qc, + basis_gates=self.TARGET_GATE_SET, + optimization_level=0, + approximation_degree=0.95, + seed_transpiler=10, + ) + + self.encode_qubits() + self.replace_gates() + return self.transpiled_qc + + def encode_qubits(self) -> None: + """Replace each logical qubit with a physical register and apply the code's encoding.""" + all_registers: list[QuantumRegister | ClassicalRegister] = [] + for i in range(self.num_logical_qubits): + data_reg = QuantumRegister(self.BLOCK_SIZE, f"q{i}") + logical_qubit = LogicalQubit( + data=data_reg, + bit_flip_syndrome=AncillaRegister(self.BIT_FLIP_SYNDROME_SIZE, f"bs{i}"), + phase_flip_syndrome=AncillaRegister(self.PHASE_FLIP_SYNDROME_SIZE, f"ps{i}"), + bit_flip_measure=ClassicalRegister(self.BIT_FLIP_SYNDROME_SIZE, f"bsm{i}"), + phase_flip_measure=ClassicalRegister(self.PHASE_FLIP_SYNDROME_SIZE, f"psm{i}"), + ) + + self.logical_qubits.append(logical_qubit) + all_registers.extend(logical_qubit.get_all_registers()) + + self.encoded_qubits[i] = True + + self.transpiled_qc = QuantumCircuit(*all_registers) + self.transpiled_qc.name = f"{self.original_qc.name}_{self.CODE_NAME}_encoded" + # Qiskit's transpile() shares nested metadata values with its input circuit. + self.transpiled_qc.metadata = deepcopy(self.original_qc.metadata) + + for logical_qubit in self.logical_qubits: + self._apply_encoding(self.transpiled_qc, logical_qubit.data) + + def replace_gates(self) -> None: + """Replace each instruction of the original circuit with its logical equivalent. + + Raises: + ValueError: If an instruction uses a qubit after the qubit's measurement. + """ + for instruction in self.original_qc.data: + gate_name = instruction.operation.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] + + # only check for actual instructions + if gate_name != "barrier": + decoded_qubits = [q for q in logical_qubit_indices if self.encoded_qubits[q] is False] + if decoded_qubits: + msg = f"Instruction {gate_name} accesses qubits {decoded_qubits} after decoding" + raise ValueError(msg) + + self._apply_gate(gate_name, logical_qubit_indices, logical_clbit_indices) + + def _apply_gate(self, gate_name: str, logical_qubit_indices: list, logical_clbit_indices: list) -> None: + """Apply a single named gate to the given logical qubit/classical-bit indices. + + Dispatch order: + 1. ``barrier`` and ``measure`` have generic handlers. + 2. A ``_logical_`` method on the subclass, if present. + 3. :attr:`TRANSVERSAL_GATES` (see :meth:`_apply_transversal_gate`). + 4. A placeholder (see :meth:`_handle_unregistered_gate`). + """ + if gate_name == "barrier": + self._handle_barrier(logical_qubit_indices) + return + if gate_name == "measure": + self._handle_measure(logical_qubit_indices[0], logical_clbit_indices[0]) + return + + handler_name = f"_logical_{gate_name}" + if hasattr(self, handler_name): + getattr(self, handler_name)(*logical_qubit_indices) + elif gate_name in self.TRANSVERSAL_GATES: + self._apply_transversal_gate(self.TRANSVERSAL_GATES[gate_name], logical_qubit_indices) + else: + self._handle_unregistered_gate(gate_name, logical_qubit_indices) + + def _handle_barrier(self, logical_qubit_indices: list[int]) -> None: + """Apply a 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 _handle_measure(self, logical_qubit_index: int, logical_classical_bit_index: int) -> None: + """Decode a logical qubit and measure it into a fresh single-bit classical register.""" + self.encoded_qubits[logical_qubit_index] = False + physical_data_register = self.logical_qubits[logical_qubit_index].data + self._apply_decoding(self.transpiled_qc, physical_data_register) + + measurement_register_name = f"meas_{logical_classical_bit_index}" + + # use existing classical register or create one + physical_measurement_register = next( + (reg for reg in self.transpiled_qc.cregs if reg.name == measurement_register_name), None + ) + + if physical_measurement_register is None: + physical_measurement_register = ClassicalRegister(1, measurement_register_name) + self.transpiled_qc.add_register(physical_measurement_register) + + self.transpiled_qc.measure(physical_data_register[0], physical_measurement_register[0]) + + def _apply_transversal_gate(self, physical_gate: Gate, logical_qubit_indices: list[int]) -> None: + """Apply ``physical_gate`` at every physical qubit position of the involved data registers. + + A syndrome cycle then runs on each involved logical qubit. + """ + physical_data_registers = [self.logical_qubits[idx].data for idx in logical_qubit_indices] + for physical_qubit_index in range(self.BLOCK_SIZE): + physical_qubits = [register[physical_qubit_index] for register in physical_data_registers] + self.transpiled_qc.append(physical_gate, physical_qubits) + + for logical_qubit_index in dict.fromkeys(logical_qubit_indices): + self.insert_syndromes(logical_qubit_index) + + def _handle_unregistered_gate(self, gate_name: str, logical_qubit_indices: list[int]) -> None: + """Realize a gate without a handler as a placeholder. + + An exact fault-tolerant realization of gates such as T needs magic-state injection or a + similar costly construction. MQT Bench is concerned with the circuit structure, so it uses + one opaque, ideal instruction on the involved data registers instead. The placeholder is + named ``_ideal_logical_``. + """ + if gate_name not in self.TARGET_GATE_SET: + msg = f"Error-correction transpilation currently does not support arbitrary gates such as {gate_name}. Currently supported: {self.TARGET_GATE_SET}" + raise ValueError(msg) + + physical_data_registers = [self.logical_qubits[idx].data for idx in logical_qubit_indices] + physical_qubits = [qubit for register in physical_data_registers for qubit in register] + + gadget = Instruction( + name=f"{self.CODE_NAME}_ideal_logical_{gate_name}", + num_qubits=len(physical_qubits), + num_clbits=0, + params=[], + ) + self.transpiled_qc.append(gadget, physical_qubits) + + for logical_qubit_index in dict.fromkeys(logical_qubit_indices): + self.insert_syndromes(logical_qubit_index) + + def insert_syndromes(self, logical_qubit_index: int) -> None: + """Run the code's syndrome extraction and correction cycle on one logical qubit. + + The transpiler calls this method after every logical gate. + + Args: + logical_qubit_index: Index of the logical qubit. + """ + self._run_syndrome_cycle(self.logical_qubits[logical_qubit_index]) + + @abstractmethod + def _apply_encoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the code's encoding to a physical data register.""" + + @abstractmethod + def _apply_decoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the code's decoding to a physical data register.""" + + @abstractmethod + def _run_syndrome_cycle(self, qubit: LogicalQubit) -> None: + """Reset ancillas, extract syndromes, and apply corrections for one logical qubit.""" 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..706329874 --- /dev/null +++ b/src/mqt/bench/error_correction/shor_transpiler.py @@ -0,0 +1,165 @@ +# 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 + +"""Transpiler that encodes circuits with Shor's 9-qubit code.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +from qiskit.circuit.library import IGate + +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, +) + +from .ec_transpiler import ECTranspiler, LogicalQubit + +if TYPE_CHECKING: + from qiskit import QuantumCircuit, QuantumRegister + from qiskit.circuit import Gate + + +class ShorTranspiler(ECTranspiler): + """Encodes a QuantumCircuit with Shor's 9-qubit code. + + X, Y, Z, and CX have handlers. Shor's 9-qubit code has no transversal H, T, or Tdg, so these + gates become placeholders. + """ + + # Structure of Shor's 9-qubit code + SHOR_BLOCK_SIZE = 3 + SHOR_NUM_BLOCKS = 3 + SHOR_PHASE_FLIP_TARGETS: ClassVar[list[int]] = [0, 3, 6] + + CODE_NAME = "shor" + BLOCK_SIZE = SHOR_BLOCK_SIZE * SHOR_NUM_BLOCKS + BIT_FLIP_SYNDROME_SIZE = 6 + PHASE_FLIP_SYNDROME_SIZE = 2 + TARGET_GATE_SET: ClassVar[list[str]] = ["id", "h", "x", "y", "z", "cx", "t", "tdg"] + TRANSVERSAL_GATES: ClassVar[dict[str, Gate]] = {"id": IGate()} + + def _apply_encoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the encoding of Shor's 9-qubit code 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 self.SHOR_PHASE_FLIP_TARGETS], + inplace=True, + ) + + # Bit flip encoding on each block + for i in range(self.SHOR_NUM_BLOCKS): + qc.compose( + get_three_qubit_bit_flip_encoding_decoding_circuit(), + qubits=physical_data_register[i * self.SHOR_BLOCK_SIZE : (i + 1) * self.SHOR_BLOCK_SIZE], + inplace=True, + ) + + def _apply_decoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the decoding of Shor's 9-qubit code to a physical data register.""" + for i in range(self.SHOR_NUM_BLOCKS): + qc.compose( + get_three_qubit_bit_flip_encoding_decoding_circuit().inverse(), + qubits=physical_data_register[i * self.SHOR_BLOCK_SIZE : (i + 1) * self.SHOR_BLOCK_SIZE], + inplace=True, + ) + qc.compose( + get_three_qubit_phase_flip_encoding_circuit().inverse(), + qubits=[physical_data_register[i] for i in self.SHOR_PHASE_FLIP_TARGETS], + inplace=True, + ) + + def _logical_x(self, logical_qubit_index: int) -> None: + """Apply logical X as Z on the first qubit of each block (Z_0 Z_3 Z_6). + + Z on one qubit of a block flips the phase of the whole block. + """ + physical_data_register = self.logical_qubits[logical_qubit_index].data + for q in (physical_data_register[i] for i in self.SHOR_PHASE_FLIP_TARGETS): + self.transpiled_qc.z(q) + self.insert_syndromes(logical_qubit_index) + + def _logical_y(self, logical_qubit_index: int) -> None: + """Apply logical Y as logical Z followed by logical X, up to a global phase.""" + 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) + for q in (physical_data_register[i] for i in self.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 logical Z as X on the three qubits of the first block (X_0 X_1 X_2).""" + 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 _logical_cx(self, control_logical_qubit_index: int, target_logical_qubit_index: int) -> None: + """Apply logical CX as physical CX gates with control and target swapped. + + Logical X acts as physical Z in Shor's 9-qubit code, so the physical CX gates must point + from the target block to the control block. + """ + control_physical_data_register = self.logical_qubits[control_logical_qubit_index].data + target_physical_data_register = self.logical_qubits[target_logical_qubit_index].data + for physical_qubit_index in range(self.BLOCK_SIZE): + control_qubit = control_physical_data_register[physical_qubit_index] + target_qubit = target_physical_data_register[physical_qubit_index] + self.transpiled_qc.cx(target_qubit, control_qubit) + + self.insert_syndromes(control_logical_qubit_index) + self.insert_syndromes(target_logical_qubit_index) + + def _run_syndrome_cycle(self, qubit: LogicalQubit) -> None: + """Extract the bit-flip and phase-flip syndromes and apply the corrections.""" + self._extract_bit_flip_syndromes(qubit) + self._extract_phase_flip_syndromes(qubit) + self._apply_error_corrections(qubit) + + def _extract_bit_flip_syndromes(self, qubit: LogicalQubit) -> None: + """Extract bit-flip syndromes for the three blocks.""" + self.transpiled_qc.reset(qubit.bit_flip_syndrome) + for i in range(self.SHOR_NUM_BLOCKS): + self.transpiled_qc.compose( + get_three_qubit_bit_flip_syndrome_extraction_circuit(), + qubits=qubit.data[i * self.SHOR_BLOCK_SIZE : (i + 1) * self.SHOR_BLOCK_SIZE] + + qubit.bit_flip_syndrome[i * 2 : (i + 1) * 2], + inplace=True, + ) + + def _extract_phase_flip_syndromes(self, qubit: LogicalQubit) -> None: + """Extract phase-flip syndromes across the blocks.""" + 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: LogicalQubit) -> 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, + ) + + 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..3393e344f --- /dev/null +++ b/src/mqt/bench/error_correction/steane_transpiler.py @@ -0,0 +1,111 @@ +# 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 + +"""Transpiler that encodes circuits with Steane's 7-qubit code.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +from qiskit.circuit.library import ( + CXGate, + CZGate, + HGate, + IGate, + SdgGate, + SGate, + XGate, + YGate, + ZGate, +) + +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, +) + +from .ec_transpiler import ECTranspiler, LogicalQubit + +if TYPE_CHECKING: + from qiskit import QuantumCircuit, QuantumRegister + from qiskit.circuit import Gate + + +class SteaneTranspiler(ECTranspiler): + """Encodes a QuantumCircuit with Steane's 7-qubit code. + + T and Tdg become placeholders. All other gates in :attr:`TARGET_GATE_SET` are transversal. + """ + + CODE_NAME = "steane" + BLOCK_SIZE = 7 + BIT_FLIP_SYNDROME_SIZE = 3 + PHASE_FLIP_SYNDROME_SIZE = 3 + TARGET_GATE_SET: ClassVar[list[str]] = [ + "id", + "x", + "y", + "z", + "h", + "s", + "sdg", + "cx", + "cz", + "t", + "tdg", + ] + # Logical S = physical Sdg on every qubit, logical Sdg = physical S on every qubit. + TRANSVERSAL_GATES: ClassVar[dict[str, Gate]] = { + "id": IGate(), + "x": XGate(), + "y": YGate(), + "z": ZGate(), + "h": HGate(), + "s": SdgGate(), + "sdg": SGate(), + "cx": CXGate(), + "cz": CZGate(), + } + + def _apply_encoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the encoding of Steane's 7-qubit code to a physical data register.""" + qc.compose( + get_seven_qubit_steane_code_encoding_circuit(), + qubits=physical_data_register[:], + inplace=True, + ) + + def _apply_decoding(self, qc: QuantumCircuit, physical_data_register: QuantumRegister) -> None: + """Apply the decoding of Steane's 7-qubit code to a physical data register.""" + qc.compose( + get_seven_qubit_steane_code_decoding_circuit(), + qubits=physical_data_register[:], + inplace=True, + ) + + def _run_syndrome_cycle(self, qubit: LogicalQubit) -> None: + """Reset the syndrome ancillas, extract the syndromes, and apply the corrections.""" + self.transpiled_qc.reset(qubit.bit_flip_syndrome) + self.transpiled_qc.reset(qubit.phase_flip_syndrome) + + self.transpiled_qc.compose( + get_seven_qubit_steane_code_syndrome_extraction_circuit(), + qubits=qubit.data[:] + qubit.bit_flip_syndrome[:] + qubit.phase_flip_syndrome[:], + inplace=True, + ) + + apply_seven_qubit_steane_code_correction( + self.transpiled_qc, + qubit.data, + qubit.bit_flip_syndrome, + qubit.phase_flip_syndrome, + qubit.bit_flip_measure, + qubit.phase_flip_measure, + ) diff --git a/tests/test_bench.py b/tests/test_bench.py index a4b6e4b6b..f7b3cddeb 100644 --- a/tests/test_bench.py +++ b/tests/test_bench.py @@ -24,13 +24,13 @@ import numpy as np import pytest from qiskit import QuantumCircuit, qasm2, qasm3, qpy -from qiskit.circuit import CircuitInstruction, ClassicalRegister, Clbit, ForLoopOp, IfElseOp, Parameter +from qiskit.circuit import Barrier, CircuitInstruction, ClassicalRegister, Clbit, ForLoopOp, IfElseOp, Parameter from qiskit.circuit.classical import expr from qiskit.circuit.library import CXGate, GraphStateGate, HGate, QFTGate, RXGate, RZGate, TGate, XGate, YGate, ZGate from qiskit.compiler import transpile from qiskit.primitives import StatevectorSampler from qiskit.providers.basic_provider import BasicSimulator -from qiskit.quantum_info import Operator, Statevector +from qiskit.quantum_info import Operator, Statevector, process_fidelity from qiskit.transpiler import ( InstructionProperties, Layout, @@ -40,6 +40,10 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import GatesInBasis, RemoveBarriers, UnrollForLoops +from mqt.bench.error_correction import get_available_encoding_names, get_transpiler +from mqt.bench.error_correction.shor_transpiler import ShorTranspiler +from mqt.bench.error_correction.steane_transpiler import SteaneTranspiler + if TYPE_CHECKING: # pragma: no cover from collections import OrderedDict from collections.abc import Callable @@ -876,6 +880,214 @@ def test_error_correction(benchmark: str, width: int, error: Gate, qubit: int) - assert len({reg.name for reg in multiple.cregs}) == len(multiple.cregs) +# Register sizes of one encoded logical qubit: data qubits, bit-flip syndrome ancillas, and +# phase-flip syndrome ancillas. Each syndrome ancilla has one classical bit. +EC_REGISTER_SIZES: dict[str, tuple[int, int, int]] = {"shor": (9, 6, 2), "steane": (7, 3, 3)} + +# Gate counts of the encoded circuits. The approximation of rotations differs between Qiskit +# versions, so the QFT entries list only the gates that the encoded circuit must contain. +EC_GATE_COUNTS: dict[tuple[str, str], dict[str, int] | set[str]] = { + ("shor", "ghz"): { + "cx": 186, + "if_else": 60, + "h": 38, + "measure": 43, + "reset": 40, + "shor_ideal_logical_h": 1, + "barrier": 1, + }, + ("shor", "bv"): { + "cx": 265, + "if_else": 108, + "measure": 74, + "reset": 72, + "h": 51, + "shor_ideal_logical_h": 6, + "z": 3, + }, + ("shor", "graphstate"): { + "cx": 435, + "if_else": 180, + "h": 78, + "measure": 123, + "reset": 120, + "shor_ideal_logical_h": 9, + "barrier": 1, + }, + ("shor", "qft"): { + "cx", + "if_else", + "measure", + "reset", + "h", + "shor_ideal_logical_t", + "shor_ideal_logical_h", + "shor_ideal_logical_tdg", + "barrier", + }, + ("shor", "z"): {"cx": 32, "if_else": 12, "measure": 8, "reset": 8, "h": 7, "x": 3}, + ("shor", "y"): {"cx": 32, "if_else": 12, "h": 7, "measure": 8, "reset": 8, "x": 3, "z": 3}, + ("steane", "ghz"): {"cx": 200, "if_else": 70, "h": 55, "measure": 33, "reset": 30, "barrier": 1}, + ("steane", "bv"): {"cx": 223, "if_else": 98, "h": 85, "measure": 44, "reset": 42, "x": 7, "cz": 7}, + ("steane", "graphstate"): { + "cx": 282, + "if_else": 126, + "h": 93, + "measure": 57, + "reset": 54, + "cz": 21, + "barrier": 1, + }, + ("steane", "qft"): { + "cx", + "if_else", + "h", + "measure", + "reset", + "steane_ideal_logical_t", + "steane_ideal_logical_tdg", + "barrier", + }, + ("steane", "z"): {"cx": 35, "if_else": 14, "h": 9, "measure": 6, "reset": 6, "z": 7}, + ("steane", "y"): {"cx": 35, "if_else": 14, "h": 9, "measure": 6, "reset": 6, "y": 7}, +} + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +@pytest.mark.parametrize("circuit", ["ghz", "bv", "graphstate", "qft", "z", "y"]) +def test_error_correction_transpiler_circuit_structure(code: str, circuit: str) -> None: + """Check the registers and gate counts of an encoded circuit.""" + logical = QuantumCircuit(1) + if circuit == "z": + logical.z(0) + elif circuit == "y": + logical.y(0) + else: + logical = get_benchmark_alg(circuit, 3) + + transpiler = get_transpiler(code)(logical) + encoded = transpiler.transpile() + assert transpiler.transpile() == encoded, "A repeated transpile call should return the same circuit" + + data, bit_flip, phase_flip = EC_REGISTER_SIZES[code] + assert sorted(reg.size for reg in encoded.qregs) == sorted([data, bit_flip, phase_flip] * logical.num_qubits), ( + "Each logical qubit should have a data register and two syndrome ancilla registers" + ) + assert sorted(reg.size for reg in encoded.cregs) == sorted( + [bit_flip, phase_flip] * logical.num_qubits + [1] * logical.num_clbits + ), "Each logical qubit should have two syndrome measurement registers, plus one register per classical bit" + + expected_gates = EC_GATE_COUNTS[code, circuit] + if isinstance(expected_gates, set): + assert expected_gates <= encoded.count_ops().keys(), "Encoded circuit should contain all expected gates" + else: + assert encoded.count_ops() == expected_gates, "Encoded circuit should have the expected gate counts" + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +@pytest.mark.parametrize("gate", [RZGate(0.3), RXGate(1.1)]) +def test_error_correction_approximates_rotations(code: str, gate: Gate) -> None: + """Check that the encoding replaces a rotation with a short Clifford+T approximation. + + The encoding adds a syndrome cycle after every logical gate, so each rotation must stay a short + gate sequence on every supported Qiskit version. The approximation must keep a process fidelity + of at least the approximation degree of 0.95. + """ + qc = QuantumCircuit(1) + qc.append(gate, [0]) + transpiler = get_transpiler(code)(qc) + transpiler.transpile() + + logical_qc = transpiler.original_qc + assert logical_qc.size() <= 50, "Rotation approximation should have at most 50 gates" + assert process_fidelity(Operator(logical_qc), Operator(qc)) >= 0.95, ( + "Rotation approximation should have a process fidelity of at least 0.95" + ) + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +def test_error_correction_transpiler_barrier_and_measure(code: str) -> None: + """Check the encoding of barriers and measurements. + + A barrier without qubits stays in the encoded circuit. Measurements into the same classical bit + share one measurement register. + """ + qc = QuantumCircuit(2, 1) + qc.append(Barrier(0), []) + qc.measure(0, 0) + qc.measure(1, 0) + encoded = get_transpiler(code)(qc).transpile() + + assert "barrier" in encoded.count_ops(), "A barrier without qubits should stay in the encoded circuit" + assert [reg.name for reg in encoded.cregs if reg.name.startswith("meas_")] == ["meas_0"], ( + "Measurements into the same classical bit should share one measurement register" + ) + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +def test_error_correction_keeps_metadata(code: str) -> None: + """The transpiler and `get_benchmark_alg` keep a copy of the input circuit's metadata.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.metadata = {"user": {"label": "input"}} + + encoded = get_transpiler(code)(qc).transpile() + benchmark = get_benchmark_alg(qc, encoding=code) + assert encoded.metadata == {"user": {"label": "input"}}, "The transpiler should copy the input metadata" + assert benchmark.metadata["user"] == {"label": "input"}, "get_benchmark_alg should keep the input metadata" + + encoded.metadata["user"]["label"] = "encoded" + benchmark.metadata["user"]["label"] = "encoded" + assert qc.metadata == {"user": {"label": "input"}}, ( + "A change to the encoded circuit's metadata should not change the input circuit" + ) + + +@pytest.mark.parametrize("code", ["shor", "steane"]) +def test_error_correction_transpiler_rejects_unsupported_circuits(code: str) -> None: + """The transpiler raises an error for circuits that the encoding does not support.""" + unsupported_gate = r"^Error-correction transpilation currently does not support arbitrary gates such as \w+\. Currently supported: .+$" + + qc = QuantumCircuit(1) + qc.delay(0) + with pytest.raises(ValueError, match=unsupported_gate): + get_transpiler(code)(qc).transpile() + + qc = QuantumCircuit(1) + qc.reset(0) + with pytest.raises(ValueError, match=unsupported_gate): + get_transpiler(code)(qc).transpile() + + # A measurement decodes the logical qubit. + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0) + with pytest.raises(ValueError, match=r"accesses qubits \[0\] after decoding"): + get_transpiler(code)(qc).transpile() + + qc = QuantumCircuit(1, 1) + with qc.if_test((0, 1)): + qc.x(0) + with pytest.raises(ValueError, match="does not support control-flow operations such as if_else"): + get_transpiler(code)(qc).transpile() + + +def test_available_encodings() -> None: + """The available encoding names should map to their transpilers and be safe to mutate.""" + names = get_available_encoding_names() + assert set(names) == {"shor", "steane"}, "The available encodings should be 'shor' and 'steane'" + + names.append("dummy_encoding") + assert "dummy_encoding" not in get_available_encoding_names(), ( + "A change to the returned list should not change the available encodings" + ) + + assert get_transpiler("shor") is ShorTranspiler, "'shor' should map to ShorTranspiler" + assert get_transpiler("steane") is SteaneTranspiler, "'steane' should map to SteaneTranspiler" + with pytest.raises(ValueError, match="not a supported error-correcting code"): + get_transpiler("dummy_encoding") + + @pytest.mark.parametrize( ( "benchmark_name", @@ -961,6 +1173,47 @@ def test_get_benchmark_alg_with_quantum_circuit() -> None: assert not qc.metadata +@pytest.mark.parametrize("encoding", ["shor", "steane"]) +@pytest.mark.parametrize("mirror", [False, True]) +def test_get_benchmark_encoding(encoding: str, mirror: bool) -> None: + """At the ALG level, `get_benchmark` encodes the benchmark and records the encoding. + + The encoding applies to the mirror circuit if one is generated. The pytest configuration turns + warnings into errors, so this test also checks that `encoding` does not warn at the ALG level. + """ + plain = get_benchmark("graphstate", BenchmarkLevel.ALG, 3, generate_mirror_circuit=mirror, seed=42) + encoded = get_benchmark( + "graphstate", BenchmarkLevel.ALG, 3, encoding=encoding, generate_mirror_circuit=mirror, seed=42 + ) + + assert encoded == get_transpiler(encoding)(plain).transpile(), ( + "get_benchmark should return the encoded plain or mirror benchmark" + ) + generation = encoded.metadata["mqt_bench_generation"] + assert generation == plain.metadata["mqt_bench_generation"] | {"encoding": encoding}, ( + "The generation record should match the plain benchmark and add the encoding" + ) + assert plain.metadata["mqt_bench_generation"]["encoding"] is None, "The plain benchmark should record no encoding" + + +@pytest.mark.parametrize( + ("level", "target"), + [ + (BenchmarkLevel.INDEP, None), + (BenchmarkLevel.NATIVEGATES, get_device("rigetti_cepheus_108")), + (BenchmarkLevel.MAPPED, get_device("rigetti_cepheus_108")), + ], +) +def test_get_benchmark_encoding_ignored_for_non_alg_level(level: BenchmarkLevel, target: Target | None) -> None: + """Test that `encoding` is ignored (with a warning) for non-ALG levels.""" + match = re.escape( + f"`encoding` is only supported for BenchmarkLevel.ALG and will be ignored for level={level.name}." + ) + with pytest.warns(UserWarning, match=match): + qc = get_benchmark(benchmark="bv", level=level, circuit_size=3, target=target, encoding="shor") + assert qc.depth() > 0, "get_benchmark should still return a benchmark when it ignores `encoding`" + + def test_get_benchmark_faulty_parameters() -> None: """Test the get_benchmark method with faulty parameters.""" match = re.escape( @@ -1004,6 +1257,9 @@ def test_get_benchmark_faulty_parameters() -> None: get_device("rigetti_cepheus_108"), 4, ) + match = re.escape("Invalid `encoding` 'wrong_encoding'. Must be one of ") + with pytest.raises(ValueError, match=match): + get_benchmark("bv", BenchmarkLevel.ALG, 3, encoding="wrong_encoding") match = re.escape( "'wrong_gateset' is not a supported gateset. Known modules: ['aqt', 'clifford_t', 'ibm', 'ionq', 'iqm', 'quantinuum', 'rigetti']" )