From bbae32f3c3ed4e6879a48233cd17051140b71b73 Mon Sep 17 00:00:00 2001 From: danmills0 Date: Fri, 10 Nov 2023 18:44:25 +0000 Subject: [PATCH] Document Stabiliser --- qermit/noise_model/noise_model.py | 10 +- qermit/noise_model/pauli_error_transpile.py | 47 +++-- qermit/noise_model/stabiliser.py | 183 ++++++++++++++------ 3 files changed, 170 insertions(+), 70 deletions(-) diff --git a/qermit/noise_model/noise_model.py b/qermit/noise_model/noise_model.py index 086c50ef..50ee267d 100644 --- a/qermit/noise_model/noise_model.py +++ b/qermit/noise_model/noise_model.py @@ -473,7 +473,7 @@ def counter_propagate( for _ in range(n_counts): stabiliser = self.random_propagate(cliff_circ, **kwargs) - if not stabiliser.is_identity(): + if not stabiliser.is_identity: error_counter.update([stabiliser]) return error_counter @@ -516,7 +516,7 @@ def random_propagate( f"Direction must be 'backward' or 'forward'. Is {direction}" ) - # For each command in the circuit, add an error ass appropriate, and + # For each command in the circuit, add an error as appropriate, and # push the total error through the command. for command in command_list: @@ -543,10 +543,12 @@ def random_propagate( for pauli, qubit in zip(error, command.args): if direction == 'backward': stabiliser.pre_apply_pauli( - pauli=pauli, qubit=qubit) + pauli=pauli, qubit=cast(Qubit, qubit) + ) elif direction == 'forward': stabiliser.post_apply_pauli( - pauli=pauli, qubit=qubit) + pauli=pauli, qubit=cast(Qubit, qubit) + ) else: raise Exception( "Direction must be 'backward' or 'forward'. " diff --git a/qermit/noise_model/pauli_error_transpile.py b/qermit/noise_model/pauli_error_transpile.py index 8925b066..552d7124 100644 --- a/qermit/noise_model/pauli_error_transpile.py +++ b/qermit/noise_model/pauli_error_transpile.py @@ -1,18 +1,39 @@ -from pytket.pauli import Pauli # type: ignore -from pytket.passes import CustomPass # type: ignore -from pytket import Circuit, OpType +from pytket.pauli import Pauli +from pytket.passes import CustomPass, BasePass +from pytket import Circuit, OpType, Qubit +from .noise_model import NoiseModel +from typing import cast -def PauliErrorTranspile(noise_model): +def PauliErrorTranspile(noise_model: NoiseModel) -> BasePass: + """Generates compiler pass which adds coherent noise to a circuit. - def add_gates(circuit): + :param noise_model: Model describing the noise to be added. Should be + a Pauli noise model. + :type noise_model: NoiseModel + :return: Compiler pass adding random coherent Pauli noise. + :rtype: BasePass + """ + def add_gates(circuit: Circuit) -> Circuit: + """Function adding random coherent Pauli errors to a circuit. + + :param circuit: Circuit to which errors are added. + :type circuit: Circuit + :raises Exception: Raised if the noise model is not a Pauli one. + :return: Circuit with additional noise operations. + :rtype: Circuit + """ + + # Initialise circuit with the same registers as input. noisy_circuit = Circuit() - for register in circuit.q_registers: - noisy_circuit.add_q_register(register) - for register in circuit.c_registers: - noisy_circuit.add_c_register(register) + for q_register in circuit.q_registers: + noisy_circuit.add_q_register(q_register) + for c_register in circuit.c_registers: + noisy_circuit.add_c_register(c_register) + # Add each command in the original circuit, + # and a pauli error if appropriate. for command in circuit.get_commands(): if command.op.type == OpType.Barrier: @@ -20,7 +41,9 @@ def add_gates(circuit): else: noisy_circuit.add_gate(command.op, command.args) + # If command has noise model defined, add a random error if command.op.type in noise_model.noisy_gates: + # Sample a random error, which may be None error = noise_model.get_error_distribution( command.op.type ).sample() @@ -30,11 +53,11 @@ def add_gates(circuit): error ): if pauli in [Pauli.X, OpType.X]: - noisy_circuit.X(qubit, opgroup='noisy') + noisy_circuit.X(cast(Qubit, qubit), opgroup='noisy') elif pauli in [Pauli.Z, OpType.Z]: - noisy_circuit.Z(qubit, opgroup='noisy') + noisy_circuit.Z(cast(Qubit, qubit), opgroup='noisy') elif pauli in [Pauli.Y, OpType.Y]: - noisy_circuit.Y(qubit, opgroup='noisy') + noisy_circuit.Y(cast(Qubit, qubit), opgroup='noisy') elif pauli in [Pauli.I]: pass else: diff --git a/qermit/noise_model/stabiliser.py b/qermit/noise_model/stabiliser.py index 89d10148..2d1595dc 100644 --- a/qermit/noise_model/stabiliser.py +++ b/qermit/noise_model/stabiliser.py @@ -1,15 +1,14 @@ from __future__ import annotations -from pytket.pauli import QubitPauliString, Pauli # type: ignore -from pytket.circuit import Qubit, OpType, Circuit # type: ignore +from pytket.pauli import QubitPauliString, Pauli +from pytket.circuit import Qubit, OpType, Circuit import math import numpy as np from numpy.random import Generator -from typing import List -# from pytket import Qubit +from typing import List, Union, Tuple class Stabiliser: - """For the manipulation of stabilisers. In particular, how they are + """For the manipulation of Pauli strings. In particular, how they are changed by the action of Clifford circuits. Note that each term in the tensor product of the stabiliser should be thought of as: (i)^{phase}X^{X_list}Z^{Z_list} @@ -27,15 +26,19 @@ def __init__( Z_list: list[int], X_list: list[int], qubit_list: list[Qubit], - phase=0 + phase: int = 0 ): - """Initialisation is by a list of qubits, and a list of 0, 1 - values indicating that a Z operator acts there. + """Initialisation is by a list of qubits, and lists of 0, 1 + values indicating that a Z or X operator acts there. :param Z_list: 0 indicates no Z, 1 indicates Z. :type Z_list: list[int] + :param X_list: 0 indicates no X, 1 indicates X. + :type X_list: list[int] :param qubit_list: List of qubits on which the stabiliser acts. :type qubit_list: list[Qubit] + :param phase: Phase as a power of i + :type phase: int """ assert all([Z in {0, 1} for Z in Z_list]) @@ -46,13 +49,33 @@ def __init__( self.phase = phase self.qubit_list = qubit_list - def is_measureable(self, qubit_list: List[Qubit]): + def is_measureable(self, qubit_list: List[Qubit]) -> bool: + """Checks if this Pauli would be measurable on the given qubits in the + computational bases. That is to say if at least one Pauli on the given + qubits anticommutes with Z. + + :param qubit_list: Qubits on which if measurable should be checked. + :type qubit_list: List[Qubit] + :raises Exception: Raised if the given qubits are not contained + in this Pauli. + :return: True if at least one Pauli on the given + qubits anticommutes with Z. False otherwise. + :rtype: bool + """ if not all(qubit in self.qubit_list for qubit in qubit_list): raise Exception( f"{qubit_list} is not a subset of {self.qubit_list}.") return any(self.X_list[qubit] == 1 for qubit in qubit_list) def reduce_qubits(self, qubit_list: List[Qubit]) -> Stabiliser: + """Reduces stabiliser onto given list of qubits. A new reduced + stabiliser is created. + + :param qubit_list: Qubits onto which stabiliser should be reduced. + :type qubit_list: List[Qubit] + :return: Reduced stabiliser. + :rtype: Stabiliser + """ return Stabiliser( Z_list=[Z for qubit, Z in self.Z_list.items() if qubit not in qubit_list], @@ -61,21 +84,13 @@ def reduce_qubits(self, qubit_list: List[Qubit]) -> Stabiliser: phase=self.phase ) - # def contains(self, sub_stabiliser): - # return ( - # all( - # qubit in self.qubit_list - # for qubit in sub_stabiliser.qubit_list - # ) and all( - # sub_stabiliser.Z_list[qubit] == self.Z_list[qubit] - # for qubit in sub_stabiliser.qubit_list - # ) and all( - # sub_stabiliser.X_list[qubit] == self.X_list[qubit] - # for qubit in sub_stabiliser.qubit_list - # ) - # ) - - def is_identity(self): + @property + def is_identity(self) -> bool: + """True is the pauli represents the all I string. + + :return: True is the pauli represents the all I string. + :rtype: bool + """ return all( Z == 0 for Z in self.Z_list.values() ) and all( @@ -87,7 +102,16 @@ def random_stabiliser( cls, qubit_list: list[Qubit], rng: Generator = np.random.default_rng(), - ): + ) -> Stabiliser: + """Generates a uniformly random stabiliser. + + :param qubit_list: Qubits on which the stabiliser acts. + :type qubit_list: list[Qubit] + :param rng: Randomness generator, defaults to np.random.default_rng() + :type rng: Generator, optional + :return: Random stabiliiser. + :rtype: Stabiliser + """ return cls( Z_list=list(rng.integers(2, size=len(qubit_list))), @@ -95,7 +119,12 @@ def random_stabiliser( qubit_list=qubit_list, ) - def dagger(self): + def dagger(self) -> Stabiliser: + """Generates the inverse of the stabiliser. + + :return: Conjugate transpose of the stabiliser. + :rtype: Stabiliser + """ # the phase is the conjugate of the original phase = self.phase @@ -103,6 +132,8 @@ def dagger(self): Z_list = list(self.Z_list.values()) X_list = list(self.X_list.values()) + # The phase is altered here as the order Z and X is reversed by + # the inversion. for Z, X in zip(Z_list, X_list): phase += 2 * Z * X phase %= 4 @@ -115,7 +146,14 @@ def dagger(self): ) @classmethod - def from_qubit_pauli_string(cls, qps): + def from_qubit_pauli_string(cls, qps: QubitPauliString) -> Stabiliser: + """Create a stabiliser from a qubit pauli string. + + :param qps: Qubit pauli string to be converted to a stabiliser. + :type qps: QubitPauliString + :return: Stabiliser created from qubit pauli string. + :rtype: Stabiliser + """ Z_list = [] X_list = [] @@ -158,15 +196,21 @@ def __hash__(self): return hash(key) def __str__(self) -> str: - # stab_str = f"X | {self.X_list}" - # stab_str += f"\nZ | {self.Z_list}" - # stab_str += f"\nphase = {self.phase_dict[self.phase]}" - # return stab_str - qubit_pauli_string, operator_phase = self.qubit_pauli_string return f"{qubit_pauli_string}, {operator_phase}" - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + """Checks for equality by checking all qubits match, and that all + Paulis on those qubits match. + + :param other: Stabiliser to compare against. + :type other: Stabiliser + :return: True is equivalent. + :rtype: bool + """ + + if not isinstance(other, Stabiliser): + return False if ( sorted(list(self.X_list.keys())) @@ -357,7 +401,12 @@ def CX(self, control_qubit: Qubit, target_qubit: Qubit): self.X_list[target_qubit] += self.X_list[control_qubit] self.X_list[target_qubit] %= 2 - def pre_multiply(self, stabiliser): + def pre_multiply(self, stabiliser: Stabiliser): + """Pre-multiply by a Stabiliser. + + :param stabiliser: Stabiliser to pre multiply by. + :type stabiliser: Stabiliser + """ for qubit in self.qubit_list: if stabiliser.X_list[qubit]: @@ -367,7 +416,15 @@ def pre_multiply(self, stabiliser): self.phase += stabiliser.phase self.phase %= 4 - def pre_apply_pauli(self, pauli, qubit): + def pre_apply_pauli(self, pauli: Union[Pauli, OpType], qubit: Qubit): + """Pre apply by a pauli on a particular qubit. + + :param pauli: Pauli to pre-apply. + :type pauli: Union[Pauli, OpType] + :param qubit: Qubit to apply Pauli to. + :type qubit: Qubit + :raises Exception: Raised if pauli is not a pauli operation. + """ if pauli in [Pauli.X, OpType.X]: self.pre_apply_X(qubit) @@ -386,6 +443,11 @@ def pre_apply_pauli(self, pauli, qubit): ) def pre_apply_X(self, qubit: Qubit): + """Pre-apply X Pauli ito qubit. + + :param qubit: Qubit to which X is pre-applied. + :type qubit: Qubit + """ self.X_list[qubit] += 1 self.X_list[qubit] %= 2 @@ -393,11 +455,24 @@ def pre_apply_X(self, qubit: Qubit): self.phase %= 4 def pre_apply_Z(self, qubit: Qubit): + """Pre-apply Z Pauli ito qubit. + + :param qubit: Qubit to which Z is pre-applied. + :type qubit: Qubit + """ self.Z_list[qubit] += 1 self.Z_list[qubit] %= 2 - def post_apply_pauli(self, pauli, qubit): + def post_apply_pauli(self, pauli: Union[Pauli, OpType], qubit: Qubit): + """Post apply a Pauli operation. + + :param pauli: Pauli to post-apply. + :type pauli: Union[Pauli, OpType] + :param qubit: Qubit to post-apply pauli to. + :type qubit: Qubit + :raises Exception: Raised if pauli is not a Pauli operation. + """ if pauli in [Pauli.X, OpType.X]: self.post_apply_X(qubit) @@ -416,11 +491,21 @@ def post_apply_pauli(self, pauli, qubit): ) def post_apply_X(self, qubit: Qubit): + """Post-apply X Pauli ito qubit. + + :param qubit: Qubit to which X is post-applied. + :type qubit: Qubit + """ self.X_list[qubit] += 1 self.X_list[qubit] %= 2 def post_apply_Z(self, qubit: Qubit): + """Post-apply Z Pauli ito qubit. + + :param qubit: Qubit to which Z is post-applied. + :type qubit: Qubit + """ self.Z_list[qubit] += 1 self.Z_list[qubit] %= 2 @@ -428,9 +513,9 @@ def post_apply_Z(self, qubit: Qubit): self.phase %= 4 def get_control_circuit(self, control_qubit: Qubit) -> Circuit: - """Circuit which acts stabiliser. + """Controlled circuit which acts stabiliser. - :return: Circuit acting stabiliser. + :return: Controlled circuit acting stabiliser. :rtype: Circuit """ @@ -482,7 +567,12 @@ def circuit(self) -> Circuit: return circ @property - def pauli_string(self): + def pauli_string(self) -> Tuple[List[Pauli], complex]: + """List of Paulis which correspond to Stabiliser, and the phase. + + :return: [description] + :rtype: Tuple[List[Pauli], complex] + """ operator_phase = self.phase paulis = [] @@ -511,23 +601,8 @@ def qubit_pauli_string(self) -> tuple[QubitPauliString, complex]: paulis, operator_phase = self.pauli_string - # operator_phase = self.phase - # paulis = [] - # for X, Z in zip(self.X_list.values(), self.Z_list.values()): - # if X == 0 and Z == 0: - # paulis.append(Pauli.I) - # elif X == 1 and Z == 0: - # paulis.append(Pauli.X) - # elif X == 0 and Z == 1: - # paulis.append(Pauli.Z) - # elif X == 1 and Z == 1: - # paulis.append(Pauli.Y) - # operator_phase += 3 - # operator_phase %= 4 - qubit_pauli_string = QubitPauliString( qubits=self.qubit_list, paulis=paulis ) - # return qubit_pauli_string, self.phase_dict[operator_phase] return qubit_pauli_string, operator_phase