Source code for qleet.analyzers.expressibility

"""Module to evaluate the expressibility of 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 state_fidelity
from scipy.spatial.distance import jensenshannon

import matplotlib.pyplot as plt
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 Expressibility(MetaExplorer): """Calculates expressibility of a parameterized quantum circuit""" def __init__( self, circuit: CircuitDescriptor, noise_model: typing.Union[ cirqNoiseModel, qiskitNoiseModel, pyquilNoiseModel, None ] = None, samples: int = 1000, ): """Constructor the the Expressibility analyzer :param circuit: input circuit as a CircuitDescriptor object :param noise_model: (dict, NoiseModel) initialization noise-model dictionary :param samples: number of samples for the experiment :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 self.expr = 0.0 self.plot_data: typing.List[np.ndarray] = []
[docs] @staticmethod def kl_divergence(prob_a: np.ndarray, prob_b: np.ndarray) -> float: """Returns KL divergence between two probabilities""" prob_a[prob_a == 0] = 1e-10 kl_div = np.sum(np.where(prob_a != 0, prob_a * np.log(prob_a / prob_b), 0)) return typing.cast(float, kl_div)
[docs] def gen_params(self) -> typing.Tuple[typing.List, typing.List]: """Generate parameters for the calculation of expressibility :returns theta (np.array): first list of parameters for the parameterized quantum circuit :returns 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] def prob_haar(self) -> np.ndarray: """Returns probability density function of fidelities for Haar Random States""" fidelity = np.linspace(0, 1, self.num_samples) num_qubits = self.circuit.num_qubits return (2 ** num_qubits - 1) * (1 - fidelity + 1e-8) ** (2 ** num_qubits - 2)
[docs] def prob_pqc(self, shots: int = 1024) -> np.ndarray: """Return probability density function of fidelities for PQC :param shots: number of shots for circuit execution :returns fidelities (np.array): np.array of fidelities """ 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 ] fidelity = np.array( [ state_fidelity(rho_a, rho_b) for rho_a, rho_b in itertools.product(theta_circuits, phi_circuits) ] ) return np.array(fidelity)
[docs] def expressibility(self, measure: str = "kld", shots: int = 1024) -> float: r"""Returns expressibility for the circuit .. math:: Expr = D_{KL}(\hat{P}_{PQC}(F; \theta) | P_{Haar}(F))\\ Expr = D_{\sqrt{JSD}}(\hat{P}_{PQC}(F; \theta) | P_{Haar}(F)) :param measure: specification for the measure used in the expressibility calculation :param shots: number of shots for circuit execution :returns pqc_expressibility: float, expressibility value :raises ValueError: if invalid measure is specified """ haar = self.prob_haar() haar_prob: np.ndarray = haar / float(haar.sum()) if len(self.circuit.parameters) > 0: fidelity = self.prob_pqc(shots) else: fidelity = np.ones(self.num_samples ** 2) bin_edges: np.ndarray pqc_hist, bin_edges = np.histogram( fidelity, self.num_samples, range=(0, 1), density=True ) pqc_prob: np.ndarray = pqc_hist / float(pqc_hist.sum()) if measure == "kld": pqc_expressibility = self.kl_divergence(pqc_prob, haar_prob) elif measure == "jsd": pqc_expressibility = jensenshannon(pqc_prob, haar_prob, 2.0) else: raise ValueError("Invalid measure provided, choose from 'kld' or 'jsd'") self.plot_data = [haar_prob, pqc_prob, bin_edges] self.expr = pqc_expressibility return pqc_expressibility
[docs] def plot(self, figsize=(6, 4), dpi=300, **kwargs): """Returns plot for expressibility visualization""" if not self.plot_data: raise ValueError("Perform expressibility calculation first") haar_prob, pqc_prob, bin_edges = self.plot_data expr = self.expr bin_middles = (bin_edges[1:] + bin_edges[:-1]) / 2.0 bin_width = bin_edges[1] - bin_edges[0] fig = plt.figure(figsize=figsize, dpi=dpi, **kwargs) plt.bar(bin_middles, haar_prob, width=bin_width, label="Haar") plt.bar(bin_middles, pqc_prob, width=bin_width, label="PQC", alpha=0.6) plt.xlim((-0.05, 1.05)) plt.ylim(bottom=0.0, top=max(max(pqc_prob), max(haar_prob)) + 0.01) plt.grid(True) plt.title(f"Expressibility: {np.round(expr,5)}") plt.xlabel("Fidelity") plt.ylabel("Probability") plt.legend() return fig