diff --git a/docs/Encoders.ipynb b/docs/Encoders.ipynb new file mode 100644 index 00000000..3a5e6249 --- /dev/null +++ b/docs/Encoders.ipynb @@ -0,0 +1,296 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c594f3e1-63c2-4d40-bec6-5ea21a78422d", + "metadata": {}, + "source": [ + "# Encoder Circuit Synthesis for CSS Codes\n", + "\n", + "QECC provides functionality for synthesizing encoding circuits of arbitrary CSS codes. An encoder for an $[[n,k,d]]$ code is an isometry that encodes $k$ logical qubits into $n$ physical qubits. \n", + "\n", + "Let's consider the synthesis of the encoding circuit of the $[[7,1,3]]$ Steane code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0234dd1e-bdb9-4030-be5d-f9952a750333", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.qecc import CSSCode\n", + "from mqt.qecc.circuit_synthesis import (\n", + " depth_optimal_encoding_circuit,\n", + " gate_optimal_encoding_circuit,\n", + " heuristic_encoding_circuit,\n", + ")\n", + "\n", + "steane_code = CSSCode.from_code_name(\"steane\")\n", + "\n", + "print(\"Stabilizers:\\n\")\n", + "print(steane_code.stabs_as_pauli_strings())\n", + "print(\"\\nLogicals:\\n\")\n", + "print(steane_code.x_logicals_as_pauli_strings())" + ] + }, + { + "cell_type": "markdown", + "id": "6e5fc2e3-9bca-4520-9ba4-68571dc4c222", + "metadata": {}, + "source": [ + "There is not a unique encoding circuit but usually we would like to obtain an encoding circuit that is optimal with respect to some metric. QECC has functionality for synthesizing gate- or depth-optimal encoding circuits. \n", + "\n", + "Under the hood, this uses the SMT solver [z3](https://github.com/Z3Prover/z3). Of course this method scales only up to a few qubits. Synthesizing depth-optimal circuits is usually faster than synthesizing gate-optimal circuits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0a64914-d6f8-48ae-b49d-4dbc425dd92d", + "metadata": {}, + "outputs": [], + "source": [ + "depth_opt, q_enc = depth_optimal_encoding_circuit(steane_code, max_timeout=5)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {depth_opt.depth()}.\")\n", + "print(f\"Circuit has {depth_opt.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "depth_opt.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c284d44e-00db-41f1-988b-47d73b790df2", + "metadata": {}, + "outputs": [], + "source": [ + "gate_opt, q_enc = gate_optimal_encoding_circuit(steane_code, max_timeout=5)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {gate_opt.depth()}.\")\n", + "print(f\"Circuit has {gate_opt.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "gate_opt.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "4d60e0a1-ea9a-45b2-9dd5-327a4db9b089", + "metadata": {}, + "source": [ + "QECC obtains optimal solutions for circuits by iteratively trying out different parameters to close in on the optimum. Each run will only be run until the number of seconds specified by `max_timeout`. If a solution is found in this time it is returned. Otherwise, `None` will be returned. \n", + "\n", + "In addition to the circuit, the synthesis methods also return the encoding qubits. All other qubits are assumed to be initialized in the $|0\\rangle$ state. \n", + "\n", + "For larger codes, synthesizing optimal circuits is not feasible. In this case, QECC provides a heuristic synthesis method that tries to use as few CNOTs with the lowest depth as possible. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0412b86-57b4-433d-8a02-5041c4aa4dd8", + "metadata": {}, + "outputs": [], + "source": [ + "heuristic_circ, q_enc = heuristic_encoding_circuit(steane_code)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {heuristic_circ.depth()}.\")\n", + "print(f\"Circuit has {heuristic_circ.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "heuristic_circ.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "eb8659b7-b3cb-4db5-9615-3969a8424456", + "metadata": {}, + "source": [ + "## Synthesizing Encoders for Concatenated Codes\n", + "\n", + "Encoders for concatenated codes can be constructed by concatenating encoding circuits. We can concatenate the $[[4,2,2]]$ code (with stabilizer generators $XXXX$ and $ZZZZ$) with itself by encoding $4$ qubits into two blocks of the code and then encoding these qubits one more time. This gives an $[[8,4,2]]$ code. The distance is still $2$ but if done the right way, some minimal-weight logicals have weight $4$.\n", + "\n", + "As an exercise, let's construct the concatenated circuit.\n", + "\n", + "We start off by defining the code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65a4c54a-a630-45b8-8f1f-96c858e4a792", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "d = 2\n", + "x_stabs = np.ones((1, 4), dtype=np.int8)\n", + "z_stabs = x_stabs\n", + "code = CSSCode(d, x_stabs, z_stabs)\n", + "\n", + "print(\"Stabilizers:\\n\")\n", + "print(code.stabs_as_pauli_strings())\n", + "print(\"\\nLogicals:\\n\")\n", + "print(code.x_logicals_as_pauli_strings())\n", + "print(code.z_logicals_as_pauli_strings())" + ] + }, + { + "cell_type": "markdown", + "id": "0962bf7e-a7db-4481-bc10-7194acaddf28", + "metadata": {}, + "source": [ + "We have to be careful with the logicals. Each *anticommuting* pair of logicals defines one logical qubit. \n", + "\n", + "As before, we synthesize the encoding circuit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95572fcb-8331-4cd4-9271-77d23c61109f", + "metadata": {}, + "outputs": [], + "source": [ + "encoder, q_enc = depth_optimal_encoding_circuit(code, max_timeout=5)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {encoder.depth()}.\")\n", + "print(f\"Circuit has {encoder.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "encoder.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "b28680cc-2bc8-4f74-a49e-a323770f2d41", + "metadata": {}, + "source": [ + "Propagating Paulis from the encoding qubits at the input to the output will not necessarily yield the exact logicals given above. But the logical operators will be stabilizer equivalent.\n", + "\n", + "Concatenating the circuits can be done as follows with qiskit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f30bdac-6411-4acb-b178-f62d863ffa85", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "from mqt.qecc.circuit_synthesis.circuit_utils import reorder_qubits\n", + "\n", + "n = 4\n", + "\n", + "first_layer = QuantumCircuit(n).tensor(encoder)\n", + "second_layer = encoder.tensor(encoder)\n", + "\n", + "initialized_qubits = set(range(2 * n)) - set(q_enc) - {q + n for q in q_enc}\n", + "qubit_mapping = {q_enc[0]: 0, q_enc[1]: 3, q_enc[0] + n: 2, q_enc[1] + n: 1}\n", + "qubit_mapping.update({q: i + 2 * len(q_enc) for i, q in enumerate(initialized_qubits)})\n", + "second_layer = reorder_qubits(second_layer, qubit_mapping)\n", + "\n", + "encoder_concat_naive = first_layer.compose(second_layer)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {encoder_concat_naive.depth()}.\")\n", + "print(f\"Circuit has {encoder_concat_naive.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "encoder_concat_naive.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "fc530584-d692-4f2c-b1ff-705577d47ebb", + "metadata": {}, + "source": [ + "Qubits $1$ and $2$ are still the encoding qubits and if we propagate Pauli $X$ and $Z$ to the output, we find that this is indeed the encoder for an $[[8,2,2]]$ code.\n", + "\n", + "This circuit has $3$ times as many CNOT gates as the encoder for the unconcatenated code because we needed to encode 3 times. Instead of concatenating the encoder circuits we can synthesize the encoders directly from the stabilizers of the concatenated code. The stabilizers of the concatenated code are the stabilizers of the original code on the respective subset of qubits with the addition of the \"encoded\" stabilizers of the inner code. We have a choice of how exactly we encode the stabilizers of the inner code. In the circuit picture, we have a choice of how we \"wire the qubits together\". Depending on how we do this, the code might have different logical operators." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b07b6d3b-6547-4a8c-887d-47e0f508acf0", + "metadata": {}, + "outputs": [], + "source": [ + "permutation = [3, 0, 2, 1]\n", + "\n", + "x_prod = (code.Lx[0] + code.Lx[1]) % 2\n", + "Hx = np.vstack((np.kron(np.eye(2, dtype=np.int8), code.Hx), np.hstack((x_prod, x_prod[permutation]))))\n", + "\n", + "z_prod = (code.Lz[0] + code.Lz[1]) % 2\n", + "Hz = np.vstack((np.kron(np.eye(2, dtype=np.int8), code.Hz), np.hstack((z_prod, z_prod[permutation]))))\n", + "\n", + "concatenated = CSSCode(4, Hx, Hz)\n", + "\n", + "print(\"Stabilizers:\\n\")\n", + "print(concatenated.stabs_as_pauli_strings())\n", + "\n", + "print(\"\\nLogicals:\\n\")\n", + "print(concatenated.x_logicals_as_pauli_strings())\n", + "print(concatenated.z_logicals_as_pauli_strings())" + ] + }, + { + "cell_type": "markdown", + "id": "2ec9a70e-84ea-4306-a426-1f47f5b6c281", + "metadata": {}, + "source": [ + "Now we can directly synthesize the encoder:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ee3daca-8dfc-459e-b449-ec3f5b040090", + "metadata": {}, + "outputs": [], + "source": [ + "encoder_concat_direct, q_enc = depth_optimal_encoding_circuit(concatenated, max_timeout=5)\n", + "\n", + "print(f\"Encoding qubits are qubits {q_enc}.\")\n", + "print(f\"Circuit has depth {encoder_concat_direct.depth()}.\")\n", + "print(f\"Circuit has {encoder_concat_direct.num_nonlocal_gates()} CNOTs.\")\n", + "\n", + "encoder_concat_direct.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "65d384a9-b1f5-491a-8691-d4ef7966b14f", + "metadata": {}, + "source": [ + "We see that the circuit is more compact then the naively concatenated one. This is because the synthesis method exploits redundancy in the check matrix of the concatenated code." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python11", + "language": "python", + "name": "python11" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/index.rst b/docs/index.rst index 63e080c1..131f0321 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,6 +28,7 @@ please let us know at our :doc:`Support ` page or by reaching out to us LightsOutDecoder EccFramework StatePrep + Encoders Publications .. toctree:: diff --git a/docs/library/Encoders.rst b/docs/library/Encoders.rst new file mode 100644 index 00000000..f8f639ff --- /dev/null +++ b/docs/library/Encoders.rst @@ -0,0 +1,14 @@ +Encoder Circuit Synthesis for CSS Codes +======================================= + +QECC provides functionality to synthesize encoder circuits for arbitrary :math:`[[n, k, d]]` quantum CSS codes. + + .. currentmodule:: mqt.qecc.circuit_synthesis + +Non-fault tolerant state preparation circuits can be synthesized using :func:`depth_optimal_encoding_circuit`, :func:`gate_optimal_encoding_circuit` and :func:`heuristic_encoding_circuit`. + + .. autofunction:: depth_optimal_encoding_circuit + + .. autofunction:: gate_optimal_encoding_circuit + + .. autofunction:: heuristic_encoding_circuit diff --git a/docs/library/Library.rst b/docs/library/Library.rst index 3c811190..60f1a02b 100644 --- a/docs/library/Library.rst +++ b/docs/library/Library.rst @@ -16,3 +16,4 @@ Library SamplePauliError LightsOutDecoder StatePrep + Encoders diff --git a/src/mqt/qecc/circuit_synthesis/circuit_utils.py b/src/mqt/qecc/circuit_synthesis/circuit_utils.py new file mode 100644 index 00000000..3041ca3f --- /dev/null +++ b/src/mqt/qecc/circuit_synthesis/circuit_utils.py @@ -0,0 +1,35 @@ +"""General circuit constructions.""" + +from __future__ import annotations + +from qiskit import QuantumCircuit, QuantumRegister + + +def reorder_qubits(circ: QuantumCircuit, qubit_mapping: dict[int, int]) -> QuantumCircuit: + """Reorders the qubits in a QuantumCircuit based on the given mapping. + + Parameters: + circuit (QuantumCircuit): The original quantum circuit. + qubit_mapping (dict[int, int]): A dictionary mapping original qubit indices to new qubit indices. + + Returns: + QuantumCircuit: A new quantum circuit with qubits reordered. + """ + # Validate the qubit_mapping + if sorted(qubit_mapping.keys()) != list(range(len(circ.qubits))) or sorted(qubit_mapping.values()) != list( + range(len(circ.qubits)) + ): + msg = "Invalid qubit_mapping: It must be a permutation of the original qubit indices." + raise ValueError(msg) + + # Create a new quantum register + num_qubits = len(circ.qubits) + new_register = QuantumRegister(num_qubits, "q") + new_circuit = QuantumCircuit(new_register) + + # Remap instructions based on the qubit_mapping + for instruction, qubits, clbits in circ.data: + new_qubits = [new_register[qubit_mapping[circ.find_bit(q)[0]]] for q in qubits] + new_circuit.append(instruction, new_qubits, clbits) + + return new_circuit diff --git a/src/mqt/qecc/circuit_synthesis/encoding.py b/src/mqt/qecc/circuit_synthesis/encoding.py index 241e68e4..a25044d1 100644 --- a/src/mqt/qecc/circuit_synthesis/encoding.py +++ b/src/mqt/qecc/circuit_synthesis/encoding.py @@ -9,6 +9,7 @@ import numpy as np import z3 +from ldpc import mod2 from ..codes import InvalidCSSCodeError from .synthesis_utils import build_css_circuit_from_cnot_list, heuristic_gaussian_elimination, optimal_elimination @@ -23,7 +24,7 @@ def heuristic_encoding_circuit( - code: CSSCode, optimize_depth: bool = True, balance_checks: bool = True + code: CSSCode, optimize_depth: bool = True, balance_checks: bool = False ) -> QuantumCircuit: """Synthesize an encoding circuit for the given CSS code using a heuristic greedy search. @@ -97,10 +98,13 @@ def gate_optimal_encoding_circuit( assert checks is not None n_checks = checks.shape[0] checks_and_logicals = np.vstack((checks, logicals)) + rank = mod2.rank(checks_and_logicals) termination_criteria = functools.partial( _final_matrix_constraint_partially_full_reduction, full_reduction_rows=list(range(checks.shape[0], checks.shape[0] + logicals.shape[0])), + rank=rank, ) + res = optimal_elimination( checks_and_logicals, termination_criteria, @@ -142,9 +146,11 @@ def depth_optimal_encoding_circuit( assert checks is not None n_checks = checks.shape[0] checks_and_logicals = np.vstack((checks, logicals)) + rank = mod2.rank(checks_and_logicals) termination_criteria = functools.partial( _final_matrix_constraint_partially_full_reduction, full_reduction_rows=list(range(checks.shape[0], checks.shape[0] + logicals.shape[0])), + rank=rank, ) res = optimal_elimination( checks_and_logicals, @@ -176,23 +182,16 @@ def _get_matrix_with_fewest_checks(code: CSSCode) -> tuple[npt.NDArray[np.int8], def _final_matrix_constraint_partially_full_reduction( - columns: npt.NDArray[z3.BoolRef | bool], full_reduction_rows: list[int] + columns: npt.NDArray[z3.BoolRef | bool], full_reduction_rows: list[int], rank: int ) -> z3.BoolRef: assert len(columns.shape) == 3 - at_least_n_row_columns = z3.PbEq( - [(z3.Or(list(columns[-1, full_reduction_rows, col])), 1) for col in range(columns.shape[2])], - len(full_reduction_rows), - ) - - fully_reduced = z3.And(at_least_n_row_columns) - partial_reduction_rows = list(set(range(columns.shape[1])) - set(full_reduction_rows)) # assert that the partial_reduction_rows are partially reduced, i.e. there are at least columns.shape[2] - (columns.shape[1] - len(full_reduction_rows)) non-zero columns partially_reduced = z3.PbEq( [(z3.Not(z3.Or(list(columns[-1, partial_reduction_rows, col]))), 1) for col in range(columns.shape[2])], - columns.shape[2] - (columns.shape[1] - len(full_reduction_rows)), + columns.shape[2] - (rank - len(full_reduction_rows)), ) # assert that there is no overlap between the full_reduction_rows and the partial_reduction_rows @@ -202,6 +201,15 @@ def _final_matrix_constraint_partially_full_reduction( has_entry_full = z3.Or(list(columns[-1, full_reduction_rows, col])) overlap_constraints.append(z3.Not(z3.And(has_entry_partial, has_entry_full))) + # assert that the full_reduction_rows are fully reduced + fully_reduced = z3.PbEq( + [ + (z3.PbEq([(columns[-1, row, col], 1) for col in range(columns.shape[2])], 1), 1) + for row in full_reduction_rows + ], + len(full_reduction_rows), + ) + return z3.And(fully_reduced, partially_reduced, z3.And(overlap_constraints))