Source code for qermit.coherent_pauli_checks.pauli_sampler

import warnings
from abc import ABC, abstractmethod
from itertools import combinations, product
from typing import List, Tuple, cast

import numpy.random
from numpy.random import Generator
from pytket import Circuit
from pytket._tket.unit_id import UnitID
from pytket.circuit import Bit, CircBox, Command, OpType, Qubit
from pytket.pauli import Pauli, QubitPauliString

from qermit.noise_model.noise_model import NoiseModel
from qermit.noise_model.qermit_pauli import QermitPauli


[docs] class PauliSampler(ABC): """Abstract base class for Pauli samplers. Pauli samples should sample Paulis to be used a checks. """
[docs] @abstractmethod def sample(self, circ: Circuit) -> List[QermitPauli]: """Sample checks for given circuit. :param circ: The circuit for which checks should be sampled. :return: Pauli checks sampled """ pass
[docs] def add_pauli_checks_to_circbox( self, circuit: Circuit, ) -> Tuple[Circuit, set[Bit]]: """Add checks to all subcircuits labeled "Clifford Subcircuit". :param circuit: Circuit to add checks to. :return: Circuit with checks added. """ # Initialise new circuit and add matching qubits. pauli_check_circuit = Circuit() for qubit in circuit.qubits: pauli_check_circuit.add_qubit(qubit) for bit in circuit.bits: pauli_check_circuit.add_bit(bit) ancilla_count = 0 postselect_bits = set() # Add each command in the circuit, wrapped by checks # if the command is a circbox named 'Clifford Subcircuit' for command in circuit.get_commands(): # Add barriers and check if appropriate if ( (command.op.type == OpType.CircBox) and (cast(CircBox, command.op).get_circuit().name is not None) and ( str(cast(CircBox, command.op).get_circuit().name).startswith( "Clifford Subcircuit" ) ) ): clifford_subcircuit = self._decompose_clifford_subcircuit_box(command) # List of Paulis to be used as checks before the subcircuit. start_stabiliser_list = self.sample( circ=clifford_subcircuit, ) # TODO: check that register names do not already exist control_qubit_list = [ Qubit(name="ancilla", index=i) for i in range( ancilla_count, ancilla_count + len(start_stabiliser_list) ) ] ancilla_count += len(start_stabiliser_list) # Add ancilla qubits and check gates for start_stabiliser, control_qubit in zip( start_stabiliser_list, control_qubit_list ): pauli_check_circuit.add_qubit(control_qubit) pauli_check_circuit.add_barrier(command.args + [control_qubit]) pauli_check_circuit.H( control_qubit, opgroup="ancilla superposition", ) stabiliser_circuit = start_stabiliser.get_control_circuit( control_qubit=control_qubit ) pauli_check_circuit.append( circuit=stabiliser_circuit, ) pauli_check_circuit.add_barrier(command.args + [control_qubit]) end_stabiliser_list = [ start_stabiliser.dagger() for start_stabiliser in start_stabiliser_list ] # Add original command pauli_check_circuit.add_gate(command.op, command.args) # Add barriers and checks if appropriate. if (command.op.type == OpType.CircBox) and ( cast(CircBox, command.op).get_circuit().name == "Clifford Subcircuit" ): # For each check, commute the pauli through # the clifford circuit. for end_stabiliser, control_qubit in zip( reversed(end_stabiliser_list), reversed(control_qubit_list) ): for clifford_command in clifford_subcircuit.get_commands(): if clifford_command.op.type == OpType.Barrier: continue # TODO: an error would be raised here if clifford_command # is not Clifford. It could be worth raising a clearer # error. end_stabiliser.apply_gate( clifford_command.op.type, clifford_command.qubits, params=clifford_command.op.params, ) pauli_check_circuit.add_barrier(command.args + [control_qubit]) # Add the end check. stabiliser_circuit = end_stabiliser.get_control_circuit( control_qubit=control_qubit ) pauli_check_circuit.append( circuit=stabiliser_circuit, ) pauli_check_circuit.H( control_qubit, opgroup="ancilla superposition", ) pauli_check_circuit.add_barrier(command.args + [control_qubit]) measure_bit = Bit( name="ancilla_measure", index=control_qubit.index, ) postselect_bits.add(measure_bit) pauli_check_circuit.add_bit(id=measure_bit) pauli_check_circuit.Measure( qubit=control_qubit, bit=measure_bit, ) return pauli_check_circuit, postselect_bits
@staticmethod def _decompose_clifford_subcircuit_box(clifford_subcircuit_box: Command) -> Circuit: """Decompose command by extracting circuit and relabelling qubits to match those of the command. :param clifford_subcircuit_box: Command to decompose. :return: Decomposed command. """ clifford_subcircuit = cast(CircBox, clifford_subcircuit_box.op).get_circuit() qubit_map = { cast(UnitID, q_subcirc): q_orig for q_subcirc, q_orig in zip( clifford_subcircuit.qubits, clifford_subcircuit_box.args ) } clifford_subcircuit.rename_units(qubit_map) return clifford_subcircuit
class DeterministicZPauliSampler(PauliSampler): """Deterministic sampler, always returning Z Pauli string.""" def sample(self, circ: Circuit) -> List[QermitPauli]: """Return Z Pauli string of length equal to the circuit. :param circ: Circuit to sample Pauli for. :return: Z Pauli string of length equal to the circuit. """ return [ QermitPauli( Z_list=[1] * circ.n_qubits, X_list=[0] * circ.n_qubits, qubit_list=circ.qubits, ) ] class DeterministicXPauliSampler(PauliSampler): """Deterministic sampler, always returning X Pauli string.""" def sample(self, circ: Circuit) -> List[QermitPauli]: """Return X Pauli string of length equal to the circuit. :param circ: Circuit to sample Pauli for. :return: X Pauli string of length equal to the circuit. """ return [ QermitPauli( Z_list=[0] * circ.n_qubits, X_list=[1] * circ.n_qubits, qubit_list=circ.qubits, ) ]
[docs] class RandomPauliSampler(PauliSampler): """Sampler returning random Pauli of appropriate length.""" def __init__( self, n_checks: int, rng: Generator = numpy.random.default_rng() ) -> None: """ :param n_checks: The number of checks to sample :param rng: Randomness generator, defaults to numpy.random.default_rng() """ self.rng = rng self.n_checks = n_checks
[docs] def sample( self, circ: Circuit, ) -> List[QermitPauli]: """Sample random Pauli of length equal to the size of the circuit. :param circ: Circuit to sample Pauli check for. :return: Random Pauli of length equal to the size of the circuit. """ # TODO: Make sure sampling is done without replacement stabiliser_list: List[QermitPauli] = [] while len(stabiliser_list) < self.n_checks: Z_list = [self.rng.integers(2) for _ in circ.qubits] X_list = [self.rng.integers(2) for _ in circ.qubits] # Avoids using the identity string as it commutes with all errors if any(Z == 1 for Z in Z_list) or any(X == 1 for X in X_list): stabiliser_list.append( QermitPauli( Z_list=Z_list, X_list=X_list, qubit_list=circ.qubits, ) ) return stabiliser_list
[docs] class OptimalPauliSampler(PauliSampler): """ Samples pauli check based on a noise model. Simulates the noise models action on clifford subcircuits in order to select checks. """ def __init__(self, noise_model: NoiseModel, n_checks: int) -> None: """ :param noise_model: The noise model to optimally pick pauli checks for. :param n_checks: The number of checks to sample. """ self.noise_model = noise_model self.n_checks = n_checks
[docs] def sample( self, circ: Circuit, ) -> List[QermitPauli]: """Samples checks for the given circuit. :param circ: The circuit to sample checks for. :return: Optimal Pauli checks. """ # TODO: assert that the registers match in this case error_counter = self.noise_model.get_effective_pre_error_distribution( cliff_circ=circ, ) # print("effective error distribution", error_counter.distribution) total_commute_prob = 0.0 total_n_pauli = 0 # smallest_commute_prob stores the proportion of shots which will # still have errors. smallest_commute_prob = 1.0 # TODO: There is probably a better way to search through this space. # Here are some ideas: # - It's better to prioritise checks that require fewer gates. # Here I am checking I last as it requires the fewest gates. # However note that IYY is checked after YII so IYY may be # picked even though YII is lighter in the case that they # have the same probability. # - It may be redundant to check Pauli.I? If a string is selected # with equal probability it may be worth it though. # - Eventually we may have to select Pauli strings at random, # but i'm not sure at what size that will be necessary. for pauli_string_list in combinations( product([Pauli.Y, Pauli.X, Pauli.Z, Pauli.I], repeat=circ.n_qubits), self.n_checks, ): if tuple([Pauli.I] * circ.n_qubits) in pauli_string_list: continue qubit_pauli_string_list = [ QubitPauliString(qubits=circ.qubits, paulis=pauli_string) for pauli_string in pauli_string_list ] commute_prob = 0.0 for error, prob in error_counter.distribution.items(): if all( error.commutes_with(qubit_pauli_string) for qubit_pauli_string in qubit_pauli_string_list ): commute_prob += prob # print(pauli_string_list, commute_prob) if smallest_commute_prob >= commute_prob: smallest_commute_prob = commute_prob smallest_commute_prob_pauli_list = qubit_pauli_string_list total_commute_prob += commute_prob total_n_pauli += 1 # average_commute_prob = total_commute_prob / total_n_pauli # print("smallest_commute_prob_pauli_list", smallest_commute_prob_pauli_list) # print("smallest_commute_prob", smallest_commute_prob) # print("average commute_prob", average_commute_prob) # if (average_commute_prob == 0) or ( # abs(1 - (smallest_commute_prob / average_commute_prob)) < 0.1 # ): # warnings.warn( # "The smallest commute probability is close to the average. " # + "Random check sampling will probably work just as well." # ) return [ QermitPauli.from_qubit_pauli_string(smallest_commute_prob_pauli) for smallest_commute_prob_pauli in smallest_commute_prob_pauli_list ]