Source code for qleet.analyzers.entanglement

"""Module to evaluate the achievable entanglement in circuits."""

import itertools
import typing

from qiskit.providers.aer.noise import NoiseModel as qiskitNoiseModel
from cirq.devices.noise_model import NoiseModel as cirqNoiseModel
from pyquil.noise import NoiseModel as pyquilNoiseModel

from qiskit.quantum_info import partial_trace
from scipy.special import comb

import numpy as np

from ..interface.metas import MetaExplorer
from ..interface.circuit import CircuitDescriptor
from ..simulators.circuit_simulators import CircuitSimulator

NOISE_MODELS = {
    "cirq": cirqNoiseModel,
    "pyquil": pyquilNoiseModel,
    "qiskit": qiskitNoiseModel,
}


[docs]class EntanglementCapability(MetaExplorer): """Calculates entangling capability of a parameterized quantum circuit""" def __init__( self, circuit: CircuitDescriptor, noise_model: typing.Union[ cirqNoiseModel, qiskitNoiseModel, pyquilNoiseModel, None ] = None, samples: int = 1000, ): """Constructor for entanglement capability plotter :param circuit: input circuit as a CircuitDescriptor object :param noise_model: (dict, NoiseModel) initialization noise-model dictionary for generating noise model :param samples: number of samples for the experiment :returns Entanglement object instance :raises ValueError: If circuit and noise model does not correspond to same framework """ super().__init__() self.circuit = circuit if noise_model is not None: if ( ( circuit.default_backend == "cirq" and isinstance(noise_model, cirqNoiseModel) ) or ( circuit.default_backend == "qiskit" and isinstance(noise_model, qiskitNoiseModel) ) or ( circuit.default_backend == "pyquil" and isinstance(noise_model, pyquilNoiseModel) ) ): self.noise_model = noise_model else: raise ValueError( f"Circuit and noise model must correspond to the same \ framework but circuit:{circuit.default_backend} and \ noise_model:{type(noise_model)} were provided." ) else: self.noise_model = None self.num_samples = samples
[docs] def gen_params(self) -> typing.Tuple[typing.List, typing.List]: """Generate parameters for the calculation of expressibility :return theta (np.array): first list of parameters for the parameterized quantum circuit :return phi (np.array): second list of parameters for the parameterized quantum circuit """ theta = [ {p: 2 * np.random.random() * np.pi for p in self.circuit.parameters} for _ in range(self.num_samples) ] phi = [ {p: 2 * np.random.random() * np.pi for p in self.circuit.parameters} for _ in range(self.num_samples) ] return theta, phi
[docs] @staticmethod def scott_helper(state, perms): """Helper function for entanglement measure. It gives trace of the output state""" dems = np.linalg.matrix_power( [partial_trace(state, list(qb)).data for qb in perms], 2 ) trace = np.trace(dems, axis1=1, axis2=2) return np.sum(trace).real
[docs] def meyer_wallach_measure(self, states, num_qubits): r"""Returns the meyer-wallach entanglement measure for the given circuit. .. math:: Q = \frac{2}{|\vec{\theta}|}\sum_{\theta_{i}\in \vec{\theta}} \Bigg(1-\frac{1}{n}\sum_{k=1}^{n}Tr(\rho_{k}^{2}(\theta_{i}))\Bigg) """ permutations = list(itertools.combinations(range(num_qubits), num_qubits - 1)) ns = 2 * sum( [ 1 - 1 / num_qubits * self.scott_helper(state, permutations) for state in states ] ) return ns.real
[docs] def scott_measure(self, states, num_qubits): r"""Returns the scott entanglement measure for the given circuit. .. math:: Q_{m} = \frac{2^{m}}{(2^{m}-1) |\vec{\theta}|}\sum_{\theta_i \in \vec{\theta}}\ \bigg(1 - \frac{m! (n-m)!)}{n!}\sum_{|S|=m} \text{Tr} (\rho_{S}^2 (\theta_i)) \bigg)\ \quad m= 1, \ldots, \lfloor n/2 \rfloor """ m = range(1, num_qubits // 2 + 1) permutations = [ list(itertools.combinations(range(num_qubits), num_qubits - idx)) for idx in m ] combinations = [1 / comb(num_qubits, idx) for idx in m] contributions = [2 ** idx / (2 ** idx - 1) for idx in m] ns = [] for ind, perm in enumerate(permutations): ns.append( contributions[ind] * sum( [ 1 - combinations[ind] * self.scott_helper(state, perm) for state in states ] ) ) return np.array(ns)
[docs] def entanglement_capability( self, measure: str = "meyer-wallach", shots: int = 1024 ) -> float: """Returns entanglement measure for the given circuit :param measure: specification for the measure used in the entangling capability :param shots: number of shots for circuit execution :returns pqc_entangling_capability (float): entanglement measure value :raises ValueError: if invalid measure is specified """ thetas, phis = self.gen_params() theta_circuits = [ CircuitSimulator(self.circuit, self.noise_model).simulate(theta, shots) for theta in thetas ] phi_circuits = [ CircuitSimulator(self.circuit, self.noise_model).simulate(phi, shots) for phi in phis ] num_qubits = self.circuit.num_qubits if measure == "meyer-wallach": pqc_entanglement_capability = self.meyer_wallach_measure( theta_circuits + phi_circuits, num_qubits ) / (2 * self.num_samples) elif measure == "scott": pqc_entanglement_capability = self.scott_measure( theta_circuits + phi_circuits, num_qubits ) / (2 * self.num_samples) else: raise ValueError( "Invalid measure provided, choose from 'meyer-wallach' or 'scott'" ) return pqc_entanglement_capability