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/main.py b/main.py new file mode 100644 index 000000000..d810e0a87 --- /dev/null +++ b/main.py @@ -0,0 +1,300 @@ +# 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 + +import qiskit as qk +from qiskit import QuantumCircuit +from qiskit.circuit import CircuitInstruction, Gate +from qiskit.circuit.library import XGate +from qiskit.quantum_info import hellinger_fidelity +from qiskit_aer.primitives import SamplerV2 + +import mqt.bench.benchmark_generation as benchmark_generation +from mqt.bench.error_correction.shor_transpiler import ShorTranspiler +from mqt.bench.error_correction.steane_transpiler import SteaneTranspiler +from tests.test_error_correction import insert_error + +# uv requirements to be added: mqt.qcec, qiskit_aer + + +def errorcode_testing(alg: str = "ghz", code: str = "shor", qubits: int = 3) -> None: + assert qubits >= 3 + + base_circuit = benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=qubits, encoding=code + ) + error_circuit = base_circuit.copy(name="error_circuit") + error_circuit = insert_error_gate(error_circuit) + benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=qubits + ) + + ### Equivalence checking + equivalent = check_equivalence(base_circuit, error_circuit) + # assert equivalent, 'Insertion of an error (flipped qubit) has lead to a new, no longer equivalent circuit' + print(f"Circuits are equivalent: {equivalent}") + + ### Simulated probabilistic similarity Base vs. Error-Inserted + # error_fidelity = compare_distributions(base_circuit, error_circuit) + # threshold = 0.95 # arbitrary guess + # assert fidelity > threshold, f'Simulated Hellinger Fidelity between base and error circuit is too low. Measured: {fidelity}, >Expected: {threshold}' + # print(f'Hellinger Fidelity with error: {error_fidelity}') + + ### Simulated probabilistic similarity Uncorrected vs. Error-Inserted + # TODO: put in error corrected circuit + #### Example for condensing qubits + # example = {'00000001111111': 3, '10101011111111': 1, '11111111010101': 2, '10101011111110' : 7} + # print(condense_counts(example,'stean')) + + """ + uncorrected_fidelity = compare_distributions(uncorrected_circuit, error_circuit, code='shor') + threshold = threshold # arbitrary guess + #assert fidelity > threshold, f'Simulated Hellinger Fidelity between uncorrected and error circuit is too low. Measured: {uncorrected_fidelity}, Expected: >{threshold}' + print(f'Hellinger Fidelity with error: {uncorrected_fidelity}') + """ + + +def run_circuit(qc: QuantumCircuit, shots: int = 1024) -> tuple[dict, QuantumCircuit]: + """Simulates the circuit using AerSimulator. + + Adds measurements to all qubits, adds new classical registers for each. + Reads out ONLY those measurements and returns their counts + + Returns: + counts of all quantum registers + + qc with measure_all() + """ + sampler = SamplerV2() + qc.measure_all() + job = sampler.run([qc], shots=shots) + result = job.result() + + # Grabbing only the desired outcomes + pub_result = result[0] + meas_bit_counts = pub_result.data.meas.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 insert_error(qc: QuantumCircuit, gate: Gate = XGate(), index: int | None = None) -> QuantumCircuit: + """Adds the specified gate at the beginning of the circuit + Flips the first qubit right after the first barrier by default. + """ + assert qc.num_qubits >= gate.num_qubits, f"Quantum Circuit has not enough qubits to accommodate gate {gate.name}" + assert index is None or index >= 0, f"Index must be >= 0, Index provided: {index}" + + # 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] + qc.data.insert(index, CircuitInstruction(gate, qubits)) + + return qc + + +def check_equivalence(qc1: qk.QuantumCircuit, qc2: qk.QuantumCircuit) -> bool: + """Uses MQT QCEC to verify if qc1 and qc2 are equivalent.""" + import mqt.qcec + from mqt.qcec.pyqcec import EquivalenceCriterion as EC + + verification_results = mqt.qcec.verify(qc1, qc2) + accepted_equivalencies = [EC.equivalent, EC.equivalent_up_to_global_phase, EC.probably_equivalent] + return verification_results.equivalence in accepted_equivalencies + + +def compare_distributions( + qc1: QuantumCircuit, qc2: QuantumCircuit, counts1: dict, counts2: dict, code1: str = "None", code2: str = "None" +) -> float: + """Simulates 2 circuits and computes the Hellinger Fidelity between their count distributions + 1 = the same, 0 = no overlap. + + If code is set to either 'steane' or 'shor' circuit error's result will be interpreted logically + """ + # print(counts1) + if code1 in ["steane", "shor"]: + counts1 = condense_counts(qc1, counts1) + # print(counts1) + + # print(counts2) + if code2 in ["steane", "shor"]: + counts2 = condense_counts(qc2, counts2) + # print(counts2) + + return hellinger_fidelity(counts1, counts2) + + +def parse_qubits(qc: qk.QuantumCircuit, physical_qubits: str): + """Takes in a measurement in physical qubits and returns the corresponding logical measurement. + + Underlying circuit must use registers named 'qx' (x in int) for each logical qubit, with results in qx[0] + """ + # remove blanks caused by classical registers + physical_qubits = physical_qubits.replace(" ", "") + + # indices + import re + + def is_q_integer(s: str) -> bool: + """Checks if s is of form 'qx' where x in int (e.g. 'q1', 'q23').""" + return bool(re.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 get_logical_classical_indices(qc, name): +# logical_cregs = sorted( +# [cr for cr in qc.cregs if cr.name.startswith(name)], +# key=lambda cr: int(cr.name.replace(name, "")) +# ) +# +# indices = [] +# +# for cr in logical_cregs: +# # assuming each logical register has size 1 +# indices.append(qc.find_bit(cr[0]).index) +# +# return indices + + +def condense_counts(qc: qk.QuantumCircuit, counts: dict[str, int]) -> dict[str, int]: + """Takes in a result dict of a decoded physical measurement and returns logical measurements + Requires decode to place the result in the first qubit of each register named 'qx', with x an integer (e.g. 'q2'). + """ + # assert code in ['shor', 'steane'], f'Unsupported error code in condense_counts(): {code}' + 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 + + +def old_main() -> None: + for alg in ["ghz", "bv", "graphstate"]: # add QFT + for code in ["shor", "stean"]: + # for qubits in range(3, 5): + qubits = 3 + # print(code) + benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=qubits, encoding=code + ) + # print(qc) + # print(" _________ ") + + code = "shor" + # Initialize circuits + t_circuit = QuantumCircuit(1) + t_circuit.h(0) + t_circuit.t(0) + t_circuit.h(0) + + xcx_circuit = QuantumCircuit(2) + # xcx_circuit.x(0) + xcx_circuit.cx(0, 1) + + h_circuit = QuantumCircuit(1) + h_circuit.z(0) + # h_circuit.h(0) + + measure_circuit = QuantumCircuit(1, 1) + measure_circuit.measure(0, 0) + + logical_circuit = measure_circuit + + # logical_circuit = benchmark_generation.get_benchmark( + # benchmark=algorithm, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=circuit_size, encoding=code + # ) + error_corrected_circuit = logical_circuit.copy() + code = "steane" + shor_transpiler = ShorTranspiler(error_corrected_circuit, add_syndromes=False) + steane_transpiler = SteaneTranspiler(logical_circuit, add_syndromes=False) + transpiler = shor_transpiler if code == "shor" else steane_transpiler + 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=ZGate(), index=16) + error_induced_circuit = insert_error(error_induced_circuit, gate=XGate()) + + # print(check_equivalence(logical_circuit, error_corrected_circuit)) + # print(check_equivalence(error_corrected_circuit, error_induced_circuit)) + + 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) + + print(" __________________________________________________________________________________________ ") + print("Logical Circuit:") + print(logical_circuit) + print(" __________________________________________________________________________________________ ") + print("Error corrected Circuit:") + print(error_corrected_circuit) + print(" __________________________________________________________________________________________ ") + print("Error Induced Circuit") + print(error_induced_circuit) + print(" __________________________________________________________________________________________ ") + + print( + compare_distributions(logical_circuit, error_corrected_circuit, logical_counts, corrected_counts, "none", code) + ) + print( + compare_distributions( + error_corrected_circuit, error_induced_circuit, corrected_counts, induced_counts, code, code + ) + ) + + +def create_gate_counts() -> None: + """Use this to create the counts for each code, algorithm and arbitrary qubit numbers.""" + import json + from pathlib import Path + + gates = {} + algs = {} + qs = {} + for code in ["shor", "steane"]: + algs = {} + for alg in ["ghz", "bv", "graphstate", "qft"]: # Bonus for "qft" (Part 3) + qs = {} + for qubits in range(3, 10): + qc = benchmark_generation.get_benchmark( + benchmark=alg, level=benchmark_generation.BenchmarkLevel.ALG, circuit_size=qubits, encoding=code + ) + qs[qubits] = qc.count_ops() + algs[alg] = qs + gates[code] = algs + + log_dir = Path(__file__).parent / "tests" + log_dir.mkdir(exist_ok=True) + + filename = log_dir / "gate_counts.json" + with Path(filename).open("w", encoding="utf-8") as f: + json.dump(gates, f, indent=4) + + +if __name__ == "__main__": + # old_main() + + create_gate_counts() diff --git a/pyproject.toml b/pyproject.toml index c5f2729ba..1fcbffabd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -250,6 +250,7 @@ aer = "aer" fom = "fom" bench = "bench" benchs = "benchs" +ket = "ket" [tool.repo-review.ignore] diff --git a/scratch.py b/scratch.py new file mode 100644 index 000000000..adf7e4e63 --- /dev/null +++ b/scratch.py @@ -0,0 +1,29 @@ +# 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 + +from qiskit import QuantumCircuit +from qiskit.quantum_info import partial_trace +from qiskit_aer import AerSimulator + +qc = QuantumCircuit(2, 1) +qc.h(0) +qc.cx(0, 1) +qc.measure(0, 0) +with qc.if_test((qc.clbits[0], 1)): + qc.x(1) +qc.save_statevector() + +sim = AerSimulator(method="statevector") +result = sim.run(qc).result() +sv = result.get_statevector() +print("Statevector:") +print(sv) + +rho = partial_trace(sv, [0]) +print("Partial trace (rho):") +print(rho) diff --git a/src/mqt/bench/benchmark_generation.py b/src/mqt/bench/benchmark_generation.py index e982a7956..f40516c2b 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: @@ -200,6 +203,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, @@ -210,6 +214,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. @@ -218,8 +223,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 @@ -480,6 +496,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, @@ -494,6 +511,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, @@ -508,6 +526,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, @@ -521,6 +540,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, @@ -535,6 +555,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. @@ -548,6 +569,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..4713f8373 --- /dev/null +++ b/src/mqt/bench/components/shor_circuit_components.py @@ -0,0 +1,172 @@ +# 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 qiskit.circuit import AncillaRegister, ClassicalRegister, QuantumCircuit, QuantumRegister + + +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..9696e1283 --- /dev/null +++ b/src/mqt/bench/components/steane_circuit_components.py @@ -0,0 +1,133 @@ +# 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 qiskit.circuit import AncillaRegister, ClassicalRegister, QuantumCircuit, QuantumRegister + + +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..12f423683 --- /dev/null +++ b/src/mqt/bench/error_correction/shor_transpiler.py @@ -0,0 +1,443 @@ +# 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))) + + # 1. Break down multi-qubit gates into single-qubit continuous gates (rx, ry, rz) and basic 2-qubit gates + continuous_basis = ["rx", "ry", "rz", "cx", "cz"] + tmp_continuous = transpile( + tmp, + basis_gates=continuous_basis, + optimization_level=3, + approximation_degree=0.95, + seed_transpiler=10, + ) + + # 2. Use Solovay-Kitaev to approximate the continuous single-qubit gates using discrete gates + from qiskit.transpiler import PassManager # noqa: PLC0415 + from qiskit.transpiler.passes.synthesis import SolovayKitaev # noqa: PLC0415 + + sk_pass = SolovayKitaev(recursion_degree=2, basis_gates=["h", "x", "z", "s", "t"]) + pm = PassManager([sk_pass]) + tmp_discrete = pm.run(tmp_continuous) + + # 3. Final transpile to target discrete basis + tmp = transpile( + tmp_discrete, + basis_gates=["h", "x", "z", "s", "t", "cx", "cz"], + optimization_level=3, + approximation_degree=0.95, + seed_transpiler=10, + ) + + 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.""" + 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.""" + 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..d33cd47da --- /dev/null +++ b/src/mqt/bench/error_correction/steane_transpiler.py @@ -0,0 +1,377 @@ +# 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 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 multi-qubit gates into single-qubit continuous gates (rx, ry, rz) and basic 2-qubit gates + continuous_basis = ["rx", "ry", "rz", "cx", "cz"] + tmp_continuous = transpile( + tmp, + basis_gates=continuous_basis, + optimization_level=3, + approximation_degree=0.95, + seed_transpiler=10, + ) + + # 2. Use Solovay-Kitaev to approximate the continuous single-qubit gates using discrete gates + from qiskit.transpiler import PassManager # noqa: PLC0415 + from qiskit.transpiler.passes.synthesis import SolovayKitaev # noqa: PLC0415 + + sk_pass = SolovayKitaev(recursion_degree=2, basis_gates=["h", "x", "z", "s", "t"]) + pm = PassManager([sk_pass]) + tmp_discrete = pm.run(tmp_continuous) + + # 3. Final transpile to target discrete basis + tmp = transpile( + tmp_discrete, + basis_gates=["h", "x", "z", "s", "t", "cx", "cz"], + optimization_level=3, + approximation_degree=0.95, + seed_transpiler=10, + ) + + 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..1a9d76dc8 --- /dev/null +++ b/tests/gate_counts.json @@ -0,0 +1,575 @@ +{ + "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": 836, + "if_else": 284, + "measure": 195, + "h": 193, + "reset": 184, + "barrier": 117, + "swap": 9, + "x": 9, + "p": 8, + "z": 3 + }, + "4": { + "cx": 1258, + "if_else": 432, + "measure": 296, + "h": 284, + "reset": 280, + "barrier": 177, + "x": 15, + "swap": 12, + "p": 12, + "z": 6 + }, + "5": { + "cx": 1680, + "if_else": 580, + "measure": 397, + "reset": 376, + "h": 375, + "barrier": 237, + "x": 21, + "p": 16, + "swap": 15, + "z": 9 + }, + "6": { + "cx": 2102, + "if_else": 728, + "measure": 498, + "reset": 472, + "h": 466, + "barrier": 297, + "x": 27, + "p": 20, + "swap": 18, + "z": 12 + }, + "7": { + "cx": 2524, + "if_else": 876, + "measure": 599, + "reset": 568, + "h": 557, + "barrier": 357, + "x": 33, + "p": 24, + "swap": 21, + "z": 15 + }, + "8": { + "cx": 2946, + "if_else": 1024, + "measure": 700, + "reset": 664, + "h": 648, + "barrier": 417, + "x": 39, + "p": 28, + "swap": 24, + "z": 18 + }, + "9": { + "cx": 3368, + "if_else": 1172, + "measure": 801, + "reset": 760, + "h": 739, + "barrier": 477, + "x": 45, + "p": 32, + "swap": 27, + "z": 21 + } + } + }, + "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": 820, + "if_else": 328, + "h": 255, + "measure": 147, + "reset": 138, + "barrier": 93, + "t": 42, + "z": 21, + "sdg": 14, + "x": 7 + }, + "4": { + "cx": 1231, + "if_else": 499, + "h": 379, + "measure": 223, + "reset": 210, + "barrier": 140, + "t": 63, + "z": 35, + "sdg": 21, + "x": 14 + }, + "5": { + "cx": 1642, + "if_else": 670, + "h": 503, + "measure": 299, + "reset": 282, + "barrier": 187, + "t": 84, + "z": 49, + "sdg": 28, + "x": 21 + }, + "6": { + "cx": 2053, + "if_else": 841, + "h": 627, + "measure": 375, + "reset": 354, + "barrier": 234, + "t": 105, + "z": 63, + "sdg": 35, + "x": 28 + }, + "7": { + "cx": 2464, + "if_else": 1012, + "h": 751, + "measure": 451, + "reset": 426, + "barrier": 281, + "t": 126, + "z": 77, + "sdg": 42, + "x": 35 + }, + "8": { + "cx": 2875, + "if_else": 1183, + "h": 875, + "measure": 527, + "reset": 498, + "barrier": 328, + "t": 147, + "z": 91, + "sdg": 49, + "x": 42 + }, + "9": { + "cx": 3286, + "if_else": 1354, + "h": 999, + "measure": 603, + "reset": 570, + "barrier": 375, + "t": 168, + "z": 105, + "sdg": 56, + "x": 49 + } + } + } +} diff --git a/tests/test_error_correction.py b/tests/test_error_correction.py new file mode 100644 index 000000000..ee225254c --- /dev/null +++ b/tests/test_error_correction.py @@ -0,0 +1,444 @@ +# 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 + +# TODO: +# uv requirements to be added: mqt.qcec, qiskit_aer + + +# What do we want to test: +# 1 function for each, split steane and shor for hardcoded sanity, combine for equvalencies +# transpilers work as intended (simply sanity checks) ✅ +# transpilers produce correct logical circuits (equivalency & simulation) +# produce circuits lead to correct results (equivalency (possible?) & simulations) +# works for all 4 given algorithms (maybe incorporate into correctness and error correction) + +# Have the ability to save the created circuits (utility function) +## save to file vs print vs logging? + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +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 + +import mqt.bench.benchmark_generation as 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: + if gate.name == "s" and code == "shor": + # this SGate includes non-unitary elements and can therefore not be evaluated properly + 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: + 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))) + 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() + # this is for inserting phase flip in steane after the first Hadamard + # error_induced_circuit = insert_error(error_induced_circuit ,gate=ZGate(), index=16) + 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: + 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 +@pytest.mark.parametrize("Error", [XGate(), ZGate()]) +@pytest.mark.parametrize("MeasureBaseX", [True, False]) +@pytest.mark.parametrize("CIRCUIT_SIZE", [3]) # range(3, 11)) +def test_errorcorrection_transpiler_correctness( + code: str, algorithm: str, Error, MeasureBaseX: bool, CIRCUIT_SIZE: int +) -> None: + """Ensures the transpiler creates error-corrected circuits which produce the same result as the orinigal logical circuit. + Afterwards an error is introduced and the test checks, whether it is corrected. + Iterates over a number of example algorithms. + """ + test_id = f"{CIRCUIT_SIZE} qubit {algorithm} on {code} with ZBasis {MeasureBaseX} 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 MeasureBaseX: + 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 = 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.95, ( + f"Error corrected circuit created does not match its logical circuit well enough for {test_id}" + ) + assert corrected_induced_fidelity >= 0.95, ( + 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: + 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 + + import json + + json_location = Path(__file__).parent / "gate_counts.json" + with Path(f"{json_location}").open("r", encoding="utf-8") as json_data: + expected_gate_counts = json.load(json_data) + json_data.close() + + 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 = XGate(), + qubit_index: int = 0, +) -> QuantumCircuit: + 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 = XGate(), index: int | None = None) -> QuantumCircuit: + """Adds the specified gate at the beginning of the circuit + Flips the first qubit right after the first barrier by default. + """ + assert qc.num_qubits >= gate.num_qubits, f"Quantum Circuit has not enough qubits to accommodate gate {gate.name}" + assert index is None or index >= 0, f"Index must be >= 0, Index provided: {index}" + + # 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 Exception(msg) + + return qc + + +def check_equivalence(qc1: qk.QuantumCircuit, qc2: qk.QuantumCircuit) -> bool: + """Uses MQT QCEC to verify if qc1 and qc2 are equivalent.""" + import mqt.qcec + from mqt.qcec.pyqcec import EquivalenceCriterion as EC + + verification_results = mqt.qcec.verify(qc1, qc2, check_partial_equivalence=True) + accepted_equivalencies = [EC.equivalent, EC.equivalent_up_to_global_phase, EC.probably_equivalent] + return verification_results.equivalence in accepted_equivalencies + + +def measure_all_named(qc: QuantumCircuit, name: str = "measurement") -> QuantumCircuit: + """Adds a classical register named 'measurement' to the circuit with one bit + per qubit, then maps each qubit i to classical bit i of that register. + + Args: + qc: The QuantumCircuit to add measurements to (modified in place). + + Returns: + The same QuantumCircuit with the register and measurements added. + """ + 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 = 8192) -> tuple[dict, QuantumCircuit]: + """Simulates the circuit using AerSimulator. + + Adds measurements to all qubits, adds new classical registers for each. + Reads out ONLY those measurements and returns their counts + + Returns: + counts of all quantum registers + + qc with measure_all() + """ + 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() + + # 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: + """Simulates 2 circuits and computes the Hellinger Fidelity between their count distributions + 1 = the same, 0 = no overlap. + + If code is set to either 'steane' or 'shor' circuit error's result will be interpreted logically + """ + 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): + """Takes in a measurement in physical qubits and returns the corresponding logical measurement. + + Underlying circuit must use registers named 'qx' (x in int) for each logical qubit, with results in qx[0] + """ + # remove blanks caused by classical registers + physical_qubits = physical_qubits.replace(" ", "") + + # indices + import re + + def is_q_integer(s: str) -> bool: + """Checks if s is of form 'qx' where x in int (e.g. 'q1', 'q23').""" + return bool(re.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]: + """Takes in a result dict of a decoded physical measurement and returns logical measurements + Requires decode to place the result in the first qubit of each register named 'qx', with x an integer (e.g. 'q2'). + """ + 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 + + +def log_circuits(circuits: dict[str, QuantumCircuit]) -> None: + from pathlib import Path + + log_dir = Path(__file__).parent / "circuit_drawings" + log_dir.mkdir(exist_ok=True) + + for name, circuit in circuits.items(): + name = log_dir / f"{name}_transpiled" + with Path(f"{name}.txt").open("w", encoding="utf-8") as f: + f.write(f"number of qubits {circuit.num_qubits}\n") + f.write(f"--- Transpiled Circuit for {name._str.upper()} ---\n\n") + f.write(str(circuit.draw(fold=-1)) + "\n") + + # fig = circuit.draw(output="mpl", fold=-1) + # fig.savefig(f"{name}.png", dpi=150, bbox_inches="tight") + # plt.close(fig)