Skip to main content
IBM Quantum Platform

Mitigación de errores de lectura para la primitiva Sampler utilizando M3

Estimación de uso: menos de un minuto en un procesador Heron r2 (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)


En segundo plano

A diferencia de la primitiva Estimador, la primitiva Muestreador no tiene soporte incorporado para la mitigación de errores. Varios de los métodos utilizados por el Estimador están diseñados específicamente para valores de expectativas y, por lo tanto, no son aplicables a la primitiva Muestreador. Una excepción es la mitigación de errores de lectura, que es un método muy eficaz que también es aplicable a la primitiva Sampler.

El complemento M3 Qiskit implementa un método eficaz para mitigar los errores de lectura. Este tutorial explica cómo utilizar el addon M3 Qiskit para mitigar el error de lectura de la primitiva Sampler.

¿Qué es un error de lectura?

Inmediatamente antes de la medición, el estado de un registro qubit es descrito por una superposición de estados de base computacional o por una matriz de densidad. A continuación, la medición del registro de qubits en un registro de bits clásico se realiza en dos pasos. Primero se realiza la medición cuántica propiamente dicha. Esto significa que el estado del registro de qubits se proyecta sobre un único estado base que se caracteriza por una cadena de 11 s y 00 s. El segundo paso consiste en leer la cadena de bits que caracteriza este estado base y escribirla en la memoria del ordenador clásico. A este paso lo llamamos lectura. Resulta que el segundo paso (lectura) incurre en más errores que el primero (proyección sobre los estados base). Esto tiene sentido cuando se recuerda que la lectura requiere detectar un estado cuántico microscópico y amplificarlo al ámbito macroscópico microscópico y amplificarlo al ámbito macroscópico. Un resonador de lectura se acopla a el qubit (transmón), experimentando así un desplazamiento de frecuencia muy pequeño. Un pulso de microondas se hace rebotar en el resonador, que experimenta a su vez pequeños cambios en sus características. A continuación, el pulso reflejado se amplifica y analiza. Se trata de un proceso proceso y está sujeto a multitud de errores.

El punto importante es que, aunque tanto la medición cuántica como la lectura están sujetas a error, esta última incurre en el error dominante, llamado error de lectura error dominante, llamado error de lectura, en el que se centra este tutorial.

Fundamentos teóricos

Si la cadena de bits muestreada (almacenada en la memoria clásica) difiere de la cadena de bits que caracteriza el estado cuántico proyectado, decimos que se ha producido un error de lectura. Se observa que estos errores son aleatorios y no están correlacionados de una muestra a otra. Se ha demostrado que es útil modelar el error de lectura como un canal clásico ruidoso. Es decir, para cada par de cadenas de bits ii y jj, existe una probabilidad fija de que un valor verdadero de jj sea se lea incorrectamente como ii.

Más concretamente, para cada par de cadenas de bits (i,j)(i, j), existe una probabilidad (condicional) Mi,j{M}_{i,j} de que se lea ii, dado que el valor verdadero es j.j. Es decir

Mi,j=Pr(readout value is itrue value is j) for i,j(0,...,2n1),(1) {M}_{i,j} = \Pr(\text{readout value is } i | \text{true value is } j) \text{ for } i,j \in (0,...,2^n - 1), \tag{1}

donde nn es el número de bits del registro de lectura. Para concretar, suponemos que ii es un entero decimal cuya representación binaria es la cadena de bits que etiqueta los estados de la base de cálculo. Llamamos matriz de asignación a la matriz 2n×2n2^n \times 2^n M{M}. Para un valor verdadero fijo jj, la suma de la probabilidad sobre todos los resultados ruidosos ii debe dar 11. Es decir

i=02n1Mi,j=1 for all j \sum_{i=0}^{2^n - 1} {M}_{i,j} = 1 \text{ for all } j

Una matriz sin entradas negativas que satisface (1) se denomina izquierda-estocástica. Una matriz izquierda-estocástica también se denomina columna-estocástica porque cada una de sus columnas suma 11. Experimentalmente determinamos valores aproximados para cada elemento Mi,j{M}_{i,j} mediante preparando repetidamente cada estado base j|j \rangle y luego calculando las frecuencias de aparición de las cadenas de bits muestreadas.

Si un experimento implica estimar una distribución de probabilidad sobre cadenas de bits de salida mediante muestreo repetido, entonces podemos usar M{M} para mitigar el error de lectura a nivel de la distribución. El primer paso consiste en repetir muchas veces un circuito fijo de interés, creando un histograma de cadenas de bits muestreadas. El histograma normalizado es la distribución de probabilidad medida sobre las 2n2^n posibles cadenas de bits, que denotamos por p~R2n{\tilde{p}} \in \mathbb{R}^{2^n}. La probabilidad (estimada) p~i{{\tilde{p}}}_i de la cadena de bits de muestreo ii es igual a la suma de todas las cadenas de bits verdaderas jj, cada una ponderada por la probabilidad de que se confunda con ii. Esta afirmación en forma de matriz es

p~=Mp,,(2) {\tilde{p}} = {M} {\vec{p}}, \tag{2},

donde p{\vec{p}} es la distribución verdadera. En otras palabras, el error de lectura tiene el efecto de multiplicar la distribución ideal sobre las cadenas de bits p{\vec{p}} por la matriz de asignación M{M} para para producir la distribución observada p~{\tilde{p}}. Hemos medido p~{\tilde{p}} y M{M}, pero no tenemos acceso directo a p{\vec{p}}. En principio, obtendremos obtendremos la verdadera distribución de cadenas de bits para nuestro circuito resolviendo numéricamente la ecuación (2) para p{\vec{p}}.

Antes de seguir adelante, conviene señalar algunas características importantes de este planteamiento ingenuo.

  • En la práctica, la ecuación (2) no se resuelve invirtiendo M{M}. Las rutinas de álgebra lineal de las bibliotecas de software emplean métodos más estables, precisos y eficientes.
  • Al estimar M{M}, asumimos que sólo se producían errores de lectura. En particular, asumimos que no hubo errores de preparación de estado y de medición cuántica - o al menos que fueron mitigados. En la medida en que se trata de una buena suposición, M{M} representa realmente error de lectura. Pero cuando usamos M{M} para corregir una distribución medida sobre cadenas de bits, no hacemos tal suposición. De hecho, esperamos que un circuito introduzca ruido, por ejemplo, errores de puerta. La distribución "verdadera sigue incluyendo los efectos de cualquier error que no se haya mitigado de otro modo.

Este método, aunque útil en algunas circunstancias, adolece de algunas limitaciones.

Los recursos de espacio y tiempo necesarios para estimar M{M} crecen exponencialmente en nn :

  • La estimación de M{M} y p~{\tilde{p}} está sujeta a errores estadísticos debidos al muestreo finito. Este ruido puede hacerse tan pequeño como se desee a costa de más disparos (hasta la escala de tiempo de los parámetros de hardware a la deriva que dan lugar a errores sistemáticos en M{M} ). Sin embargo, si no se hacen suposiciones sobre las cadenas de bits observadas al realizar la mitigación, el número de disparos necesarios para estimar M{M} crece al menos exponencialmente en nn.
  • M{M} es una matriz 2n×2n2^n \times 2^n. En n>10n>10, la cantidad de memoria necesaria para almacenar M{M} es mayor que la memoria disponible en un portátil potente.

Otras limitaciones son:

  • La distribución recuperada p{\vec{p}} puede tener una o más probabilidades negativas (sin dejar de sumar uno). Una solución es minimizar Mpp~2||{M} {\vec{p}} - {\tilde{p}}||^2 con la restricción de que cada entrada de p{\vec{p}} sea no negativa. Sin embargo, el tiempo de ejecución de este es varios órdenes de magnitud superior al de la resolución directa de la ecuación (2).
  • Este procedimiento de mitigación funciona a nivel de una distribución de probabilidad sobre cadenas de bits. En concreto, no puede corregir un error en una cadena de bits cadena de bits observada.

Complemento Qiskit M3 : escalado a cadenas de bits más largas

La resolución de la ecuación (2) mediante rutinas estándar de álgebra lineal numérica está limitada a cadenas de bits de no más de unos 10 bits. M3 puede manejar cadenas de bits mucho más largas. Dos propiedades clave de M3 que lo hacen posible son:

  • Las correlaciones en el error de lectura de orden tres y superior entre colecciones de bits se suponen despreciables y se ignoran. En principio, a costa de más disparos, también se podrían estimar correlaciones más altas.
  • En lugar de construir M{M} explícitamente, utilizamos una matriz efectiva mucho más pequeña que registra probabilidades sólo para las cadenas de bits recogidas al construir p~{\tilde{p}}.

A grandes rasgos, el procedimiento funciona del siguiente modo.

En primer lugar, construimos bloques de construcción a partir de los cuales podemos elaborar una descripción simplificada y eficaz de M{M}. A continuación, ejecutamos repetidamente el circuito de interés y recopilamos cadenas de bits que utilizamos para construir tanto p~{\tilde{p}} como, con la ayuda de los bloques de construcción, una descripción efectiva de M{M}.

Más exactamente,

  • Las matrices de asignación de un qubit se estiman para cada qubit. Para ello preparamos repetidamente el registro de qubits en el estado todo-cero 0...0|0 ... 0 \rangle y después en el estado todo-uno 1...1|1 ... 1 \rangle, y registramos la probabilidad de que cada qubit se lea incorrectamente incorrectamente.

  • Las correlaciones de orden tres o superior se consideran despreciables y no se tienen en cuenta.

    En su lugar construimos un número nn de 2×22 \times 2 matrices de asignación single-qubit y un número n(n1)/2n(n-1)/2 de matrices de asignación de dos qubits 4×44 \times 4 de dos qubits. Estas matrices de asignación de uno y dos qubits se almacenan para su uso posterior uso posterior.

  • Tras muestrear repetidamente un circuito para construir p~{\tilde{p}}, construimos una aproximación efectiva a M{M} utilizando únicamente cadenas de bits que se muestrean al construir p~{\tilde{p}}. Esta matriz efectiva se construye utilizando las matrices de uno y dos qubits descritas en el punto anterior. La dimensión lineal de esta matriz es como máximo del orden del número de disparos utilizados en la construcción de p~{\tilde{p}}, que es mucho menor que la dimensión 2n2^n de la matriz de asignación completa M{M}.

Para más detalles técnicos sobre M3, puede consultar Scalable Mitigation of Measurement Errors on Quantum Computers.

Aplicación de M3 a un algoritmo cuántico

Aplicaremos M3 's readout mitigation to the hidden shift problem. El problema del desplazamiento oculto, y otros problemas estrechamente relacionados, como el problema del subgrupo oculto, se concibieron originalmente en un entorno tolerante a fallos (más concretamente, antes de que se demostrara que las QPU tolerantes a fallos eran posibles). Pero también se estudian con los procesadores disponibles. Un ejemplo de aceleración exponencial algorítmica obtenida para una variante del problema de turnos ocultos obtenida en QPUs de 127 qubits IBM® puede encontrarse en este artículo ( arXiv version ).

A continuación, toda la aritmética es booleana. Es decir, para a,bZ2={0,1}a, b \in \mathbb{Z}_2 = \{0, 1\}, suma, a+ba + b es la función lógica XOR. Además, la multiplicación a×ba \times b (o aba b ) es la función lógica AND. Para x,y{0,1}nx, y \in \{0, 1\}^n, x+yx + y se define mediante la aplicación bit a bit de XOR. El producto punto :Z2nZ2\cdot: {\mathbb{Z}_2^n} \rightarrow \mathbb{Z}_2 se define por xy=ixiyix \cdot y = \sum_i x_i y_i.

Operador de Hadamard y transformada de Fourier

En la implementación de algoritmos cuánticos, es muy común utilizar el operador de Hadamard como transformada de Fourier. Los estados base computacionales se denominan a veces estados clásicos. Están en una relación de uno a uno con las cadenas de bits clásicas. El operador de Hadamard nn -qubit sobre estados clásicos puede verse como una transformada de Fourier sobre el hipercubo booleano:

Hn=12nx,yZ2n(1)xyyx.H^{\otimes n} = \frac{1}{\sqrt{2^n}} \sum_{x,y \in {\mathbb{Z}_2^n}} (-1)^{x \cdot y} {|{y}\rangle}{\langle{x}|}.

Consideremos un estado s{|{s}\rangle} correspondiente a la cadena de bits fija ss. Aplicando HnH^{\otimes n}, y utilizando xs=δx,s{\langle {x}|{s}\rangle} = \delta_{x,s}, vemos que la transformada de Fourier de s{|{s}\rangle} puede escribirse como

Hns=12nyZ2n(1)syy. H^{\otimes n} {|{s}\rangle} = \frac{1}{\sqrt{2^n}} \sum_{y \in {\mathbb{Z}_2^n}} (-1)^{s \cdot y} {|{y}\rangle}.

El Hadamard es su propio inverso, es decir, HnHn=(HH)n=InH^{\otimes n} H^{\otimes n} = (H H)^{\otimes n} = I^{\otimes n}. Por lo tanto, la transformada de Fourier inversa es el mismo operador, HnH^{\otimes n}. Explícitamente, tenemos,

s=HnHns=Hn12nyZ2n(1)syy. {|{s}\rangle} = H^{\otimes n} H^{\otimes n} {|{s}\rangle} = H^{\otimes n} \frac{1}{\sqrt{2^n}} \sum_{y \in {\mathbb{Z}_2^n}} (-1)^{s \cdot y} {|{y}\rangle}.

El problema del cambio oculto

Consideramos un ejemplo sencillo de un problema de desplazamiento oculto. El problema consiste en identificar un desplazamiento constante en la entrada de una función. La función que consideramos es el producto punto. Es el miembro más sencillo de una gran clase de funciones que admiten una aceleración cuántica para el problema del desplazamiento oculto mediante técnicas similares a las que se presentan a continuación.

Sea x,yZ2mx,y \in {\mathbb{Z}_2^m} cadenas de bits de longitud mm. Definimos f:Z2m×Z2m{1,1}{f}: {\mathbb{Z}_2^m} \times {\mathbb{Z}_2^m} \rightarrow \{-1,1\} por

f(x,y)=(1)xy. {f}(x, y) = (-1)^{x \cdot y}.

Sea a,bZ2ma,b \in {\mathbb{Z}_2^m} cadenas de bits fijas de longitud mm. Definimos además g:Z2m×Z2m{1,1}g: {\mathbb{Z}_2^m} \times {\mathbb{Z}_2^m} \rightarrow \{-1,1\} por

g(x,y)=f(x+a,y+b)=(1)(x+a)(y+b), g(x, y) = {f}(x+a, y+b) = (-1)^{(x+a) \cdot (y+b)},

donde aa y bb son parámetros (ocultos). Se nos dan dos cajas negras, una que implementa ff, y la otra gg. Suponemos que sabemos que calculan las funciones definidas anteriormente, excepto que no conocemos ni aa ni bb. El juego consiste en determinar las cadenas de bits ocultas (shifts) aa y bb haciendo consultas a ff y gg. Está claro que si jugamos el juego de forma clásica necesitamos consultas a O(2m)O(2m) para determinar aa y bb. Por ejemplo, podemos consultar gg con todos los pares de cadenas tales que un elemento del par sea todo ceros, y el otro elemento tenga exactamente un elemento establecido en 11. En cada consulta, aprendemos un elemento de aa o bb. Sin embargo, veremos que, si las cajas negras se implementan como circuitos cuánticos, podemos determinar aa y bb con una sola consulta a cada uno de ff y gg.

En el contexto de la complejidad algorítmica, una caja negra se denomina oráculo. Además de ser opaco, un oráculo tiene la propiedad de que consume la entrada y produce la salida instantáneamente, sin añadir nada al presupuesto de complejidad del algoritmo en el que está integrado. De hecho, en el caso que nos ocupa, los oráculos que implementan ff y gg son eficientes.

Circuitos cuánticos para ff y gg

Necesitamos los siguientes ingredientes para implementar ff y gg como circuitos cuánticos.

Para estados clásicos de un solo qubit x1,y1{|{x_1}\rangle}, {|{y_1}\rangle}, con x1,y1Z2x_1,y_1 \in \mathbb{Z}_2, la puerta controlada- ZZ CZ{CZ} puede escribirse como

CZx1y1x1=(1)x1y1x1x1y1.{CZ} {|{x_1}\rangle}{|{y_1}\rangle}{x_1} = (-1)^{x_1 y_1} {|{x_1}\rangle}{x_1}{|{y_1}\rangle}.

Operaremos con mm puertas CZ, una en (x1,y1)(x_1, y_1), y otra en (x2,y2)(x_2, y_2), y así sucesivamente, a través de (xm,ym)(x_m, y_m). Llamamos a este operador CZx,y{CZ}_{x,y}.

Uf=CZx,yU_f = {CZ}_{x,y} es una versión cuántica de f=f(x,y){f} = {f}(x,y) :

Ufxy=CZx,yxy=(1)xyxy.%\CZ_{x,y} {|#1\rangle}{z} = U_f {|{x}\rangle}{|{y}\rangle} = {CZ}_{x,y} {|{x}\rangle}{|{y}\rangle} = (-1)^{x \cdot y} {|{x}\rangle}{|{y}\rangle}.

También necesitamos implementar un desplazamiento de cadena de bits. Denotamos el operador en el registro xx Xa1XamX^{a_1}\cdots X^{a_m} por XaX_a e igualmente en el registro yy Xb=Xb1XbmX_b = X^{b_1}\cdots X^{b_m}. Estos operadores aplican XX siempre que un bit sea 11, y la identidad II siempre que sea 00. Entonces tenemos

XaXbxy=x+ay+b. X_a X_b {|{x}\rangle}{|{y}\rangle} = {|{x+a}\rangle}{|{y+b}\rangle}.

La segunda caja negra gg se implementa mediante el unitario UgU_g, dado por

Ug=XaXbCZx,yXaXb.%U_g {|{x}\rangle}{|{y}\rangle} = X_aX_b \CZ_{x,y} X_aX_b {|{x}\rangle}{|{y}\rangle}. U_g = X_aX_b {CZ}_{x,y} X_aX_b.

Para verlo, aplicamos los operadores de derecha a izquierda al estado xy{|{x}\rangle}{|{y}\rangle}. Primero

XaXbxy=x+ay+b. X_a X_b {|{x}\rangle}{|{y}\rangle} = {|{x+a}\rangle}{|{y+b}\rangle}.

A continuación,

CZx,yx+ay+b=(1)(x+a)(y+b)x+ay+b. {CZ}_{x,y} {|{x+a}\rangle}{|{y+b}\rangle} = (-1)^{(x+a)\cdot (y+b)} {|{x+a}\rangle}{|{y+b}\rangle}.

Por último,

XaXb(1)(x+a)(y+b)x+ay+b=(1)(x+a)(y+b)xy, X^a X^b (-1)^{(x+a)\cdot (y+b)} {|{x+a}\rangle}{|{y+b}\rangle} = (-1)^{(x+a)\cdot (y+b)} {|{x}\rangle}{|{y}\rangle},

que es, en efecto, la versión cuántica de f(x+a,y+b)f(x+a, y+b).

El algoritmo de cambio oculto

Ahora unimos las piezas para resolver el problema del turno oculto. Comenzamos aplicando Hadamards a los registros inicializados al estado todo-cero.

H2m=HmHm0m0m=122mx,yZ2m(1)xyxy.H^{\otimes 2m} = H^{\otimes m} \otimes H^{\otimes m} {{|{0}\rangle}^{\otimes m}}{{|{0}\rangle}^{\otimes m}} = \frac{1}{\sqrt{2^{2m}}} \sum_{x, y \in {\mathbb{Z}_2^m}} (-1)^{x \cdot y} {|{x}\rangle}{|{y}\rangle}.

A continuación, consultamos el oráculo gg para llegar a

UgH2m0m0m=122mx,yZ2m(1)(x+a)(y+b)xyU_g H^{\otimes 2m} {{|{0}\rangle}^{\otimes m}}{{|{0}\rangle}^{\otimes m}} = \frac{1}{\sqrt{2^{2m}}} \sum_{x, y \in {\mathbb{Z}_2^m}} (-1)^{(x+a) \cdot (y+b)} {|{x}\rangle}{|{y}\rangle} 122mx,yZ2m(1)xy+xb+yaxy.\approx \frac{1}{\sqrt{2^{2m}}} \sum_{x, y \in {\mathbb{Z}_2^m}} (-1)^{x \cdot y + x \cdot b + y \cdot a} {|{x}\rangle}{|{y}\rangle}.

En la última línea, omitimos el factor de fase global constante (1)ab(-1)^{a \cdot b}, y denotamos la igualdad hasta una fase por \approx. A continuación, aplicando el oráculo ff introducimos otro factor de (1)xy(-1)^{x \cdot y}, anulando el ya presente. Entonces tenemos:

UfUgH2m0m0m122mx,yZ2m(1)xb+yaxy.U_f U_g H^{\otimes 2m} {{|{0}\rangle}^{\otimes m}}{{|{0}\rangle}^{\otimes m}} \approx \frac{1}{\sqrt{2^{2m}}} \sum_{x, y \in {\mathbb{Z}_2^m}} (-1)^{x \cdot b + y \cdot a} {|{x}\rangle}{|{y}\rangle}.

El último paso es aplicar la transformada inversa de Fourier, H2m=HmHmH^{\otimes 2m} = H^{\otimes m} \otimes H^{\otimes m}, dando como resultado

H2mUfUgH2m0m0mba.H^{\otimes 2m} U_f U_g H^{\otimes 2m} {{|{0}\rangle}^{\otimes m}}{{|{0}\rangle}^{\otimes m}} \approx {|{b}\rangle}{|{a}\rangle}.

El circuito está terminado. En ausencia de ruido, el muestreo de los registros cuánticos devolverá devolverá las cadenas de bits b,ab, a con la probabilidad 11.

El producto interior booleano es un ejemplo de las llamadas funciones dobladas. No definiremos aquí las funciones curvas sino que nos limitaremos a señalar que "son máximamente resistentes contra ataques que buscan explotar una dependencia de las salidas en algún subespacio lineal de las entradas" Esta cita es del artículo Quantum algorithms for highly non-linear Boolean functions, que da algoritmos eficientes de desplazamiento oculto para varias clases de funciones curvas. El algoritmo de este tutorial aparece en la sección 3.1 del artículo.

En el caso más general, el circuito para encontrar un desplazamiento oculto sZns \in \mathbb{Z}^n es

HnUf~HnUgHn0n=s. H^{\otimes n} U_{\tilde{f}} H^{\otimes n} U_g H^{\otimes n} {|{0}\rangle}^{\otimes n} = {|{s}\rangle}.

En el caso general, ff y gg son funciones de una sola variable. Nuestro ejemplo del producto interior tiene esta forma si dejamos que f(x,y)f(z)f(x, y) \to f(z), siendo zz igual a la concatenación de xx y yy, y ss igual a la concatenación de aa y bb. El caso general requiere exactamente dos oráculos: Un oráculo para gg y otro para f~\tilde{f}, donde este último es una función conocida como el dual de la función doblada ff. La función producto interior tiene la propiedad autodual f~=f\tilde{f}=f.

En nuestro circuito para el desplazamiento oculto sobre el producto interior omitimos la capa intermedia de Hadamards que aparece en el circuito para el caso general. Aunque en el caso general esta capa es necesaria, ahorramos un poco de profundidad al omitirla, a expensas de un poco de postprocesamiento, ya que la salida es ba{|{b}\rangle}{|{a}\rangle} en lugar de la deseada ab{|{a}\rangle}{|{b}\rangle}.


Requisitos

Antes de empezar este tutorial, asegúrate de que tienes instalado lo siguiente:

  • Qiskit SDK v2.1 o posterior, con soporte de visualización
  • Qiskit Runtime v0.41 o posterior (pip install qiskit-ibm-runtime)
  • M3 Qiskit addon v3.0 (pip install mthree)

Configuración

from collections.abc import Iterator, Sequence
from random import Random
from qiskit.circuit import (
    CircuitInstruction,
    QuantumCircuit,
    QuantumRegister,
    Qubit,
)
from qiskit.circuit.library import CZGate, HGate, XGate
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
import timeit
import matplotlib.pyplot as plt
from qiskit_ibm_runtime import SamplerV2 as Sampler
import mthree

Paso 1: Asignar entradas clásicas a un problema cuántico

En primer lugar, escribimos las funciones para implementar el problema del desplazamiento oculto como QuantumCircuit.

def apply_hadamards(qubits: Sequence[Qubit]) -> Iterator[CircuitInstruction]:
    """Apply a Hadamard gate to every qubit."""
    for q in qubits:
        yield CircuitInstruction(HGate(), [q], [])


def apply_shift(
    qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
    """Apply X gates where the bits of the shift are equal to 1."""
    for i, q in zip(range(shift.bit_length()), qubits):
        if shift >> i & 1:
            yield CircuitInstruction(XGate(), [q], [])


def oracle_f(qubits: Sequence[Qubit]) -> Iterator[CircuitInstruction]:
    """Apply the f oracle."""
    for i in range(0, len(qubits) - 1, 2):
        yield CircuitInstruction(CZGate(), [qubits[i], qubits[i + 1]])


def oracle_g(
    qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
    """Apply the g oracle."""
    yield from apply_shift(qubits, shift)
    yield from oracle_f(qubits)
    yield from apply_shift(qubits, shift)


def determine_hidden_shift(
    qubits: Sequence[Qubit], shift: int
) -> Iterator[CircuitInstruction]:
    """Determine the hidden shift."""
    yield from apply_hadamards(qubits)
    yield from oracle_g(qubits, shift)
    # We omit this layer in exchange for post processing
    # yield from apply_hadamards(qubits)
    yield from oracle_f(qubits)
    yield from apply_hadamards(qubits)


def run_hidden_shift_circuit(n_qubits, rng):
    hidden_shift = rng.getrandbits(n_qubits)

    qubits = QuantumRegister(n_qubits, name="q")
    circuit = QuantumCircuit.from_instructions(
        determine_hidden_shift(qubits, hidden_shift), qubits=qubits
    )
    circuit.measure_all()
    # Format the hidden shift as a string.
    hidden_shift_string = format(hidden_shift, f"0{n_qubits}b")
    return (circuit, hidden_shift, hidden_shift_string)


def display_circuit(circuit):
    return circuit.remove_final_measurements(inplace=False).draw(
        "mpl", idle_wires=False, scale=0.5, fold=-1
    )

Empezaremos con un pequeño ejemplo:

n_qubits = 6
random_seed = 12345
rng = Random(random_seed)
circuit, hidden_shift, hidden_shift_string = run_hidden_shift_circuit(
    n_qubits, rng
)

print(f"Hidden shift string {hidden_shift_string}")

display_circuit(circuit)

Output:

Hidden shift string 011010
Output of the previous code cell

Paso 2: Optimizar los circuitos para la ejecución del hardware cuántico

job_tags = [
    f"shift {hidden_shift_string}",
    f"n_qubits {n_qubits}",
    f"seed = {random_seed}",
]
job_tags

Output:

['shift 011010', 'n_qubits 6', 'seed = 12345']
# Uncomment this to run the circuits on a quantum computer on IBMCloud.
service = QiskitRuntimeService()
backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=100
)

# from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
# backend = FakeMelbourneV2()
# backend.refresh(service)

print(f"Using backend {backend.name}")


def get_isa_circuit(circuit, backend):
    pass_manager = generate_preset_pass_manager(
        optimization_level=3, backend=backend, seed_transpiler=1234
    )
    isa_circuit = pass_manager.run(circuit)
    return isa_circuit


isa_circuit = get_isa_circuit(circuit, backend)
display_circuit(isa_circuit)

Output:

Using backend ibm_kingston
Output of the previous code cell

Paso 3: Ejecutar circuitos utilizando Qiskit primitives

# submit job for solving the hidden shift problem using the Sampler primitive
NUM_SHOTS = 50_000


def run_sampler(backend, isa_circuit, num_shots):
    sampler = Sampler(mode=backend)
    sampler.options.environment.job_tags
    pubs = [(isa_circuit, None, NUM_SHOTS)]
    job = sampler.run(pubs)
    return job


def setup_mthree_mitigation(isa_circuit, backend):
    # retrieve the final qubit mapping so mthree knows which qubits to calibrate
    qubit_mapping = mthree.utils.final_measurement_mapping(isa_circuit)

    # submit jobs for readout error calibration
    mit = mthree.M3Mitigation(backend)
    mit.cals_from_system(qubit_mapping, rep_delay=None)

    return mit, qubit_mapping
job = run_sampler(backend, isa_circuit, NUM_SHOTS)
mit, qubit_mapping = setup_mthree_mitigation(isa_circuit, backend)

Paso 4: Procesamiento posterior y devolución de los resultados en formato clásico

En la discusión teórica anterior, determinamos que para la entrada abab, esperamos la salida baba. Una complicación adicional es que, para tener un circuito más simple (pre-transpilado), insertamos las puertas CZ necesarias entre los pares de qubits vecinos pares de qubits vecinos. Esto equivale a intercalar las cadenas de bits aa y bb como a1b1a2b2a1 b1 a2 b2 \ldots. La cadena de salida baba se intercalará de forma similar: b1a1b2a2b1 a1 b2 a2 \ldots. La función unscramble a continuación transforma la cadena de salida de b1a1b2a2b1 a1 b2 a2 \ldots en a1b1a2b2a1 b1 a2 b2 \ldots para que las cadenas de entrada y salida puedan compararse directamente.

# retrieve bitstring counts
def get_bitstring_counts(job):
    result = job.result()
    pub_result = result[0]
    counts = pub_result.data.meas.get_counts()
    return counts, pub_result
counts, pub_result = get_bitstring_counts(job)

La distancia de Hamming entre dos cadenas de bits es el número de índices en los que difieren los bits.

def hamming_distance(s1, s2):
    weight = 0
    for c1, c2 in zip(s1, s2):
        (c1, c2) = (int(c1), int(c2))
        if (c1 == 1 and c2 == 1) or (c1 == 0 and c2 == 0):
            weight += 1

    return weight
# Replace string of form a1b1a2b2... with b1a1b2a1...
# That is, reverse order of successive pairs of bits.
def unscramble(bitstring):
    ps = [bitstring[i : i + 2][::-1] for i in range(0, len(bitstring), 2)]
    return "".join(ps)


def find_hidden_shift_bitstring(counts, hidden_shift_string):
    # convert counts to probabilities
    probs = {
        unscramble(bitstring): count / NUM_SHOTS
        for bitstring, count in counts.items()
    }

    # Retrieve the most probable bitstring.
    most_probable = max(probs, key=lambda x: probs[x])

    print(f"Expected hidden shift string: {hidden_shift_string}")
    if most_probable == hidden_shift_string:
        print("Most probable bitstring matches hidden shift 😊.")
    else:
        print("Most probable bitstring didn't match hidden shift ☹️.")
    print("Top 10 bitstrings and their probabilities:")
    display(
        {
            k: (v, hamming_distance(hidden_shift_string, k))
            for k, v in sorted(
                probs.items(), key=lambda x: x[1], reverse=True
            )[:10]
        }
    )

    return probs, most_probable
probs, most_probable = find_hidden_shift_bitstring(
    counts, hidden_shift_string
)

Output:

Expected hidden shift string: 011010
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their probabilities:
{'011010': (0.9743, 6),
 '001010': (0.00812, 5),
 '010010': (0.0063, 5),
 '011000': (0.00554, 5),
 '011011': (0.00492, 5),
 '011110': (0.00044, 5),
 '001000': (0.00012, 4),
 '010000': (8e-05, 4),
 '001011': (6e-05, 4),
 '000010': (6e-05, 4)}

Registremos la probabilidad de la cadena de bits más probable antes de aplicar la mitigación de errores de lectura con M3.

max_probability_before_M3 = probs[most_probable]
max_probability_before_M3

Output:

0.9743

Ahora aplicamos la corrección de lectura aprendida por M3 a los recuentos. La función apply_corrections devuelve una distribución cuasi-probabilística. Esta es una lista de float objetos que suman un 11 e. Sin embargo, algunos valores pueden ser negativos.

def perform_mitigation(mit, counts, qubit_mapping):
    # mitigate readout error
    quasis = mit.apply_correction(counts, qubit_mapping)

    # print results
    most_probable_after_m3 = unscramble(max(quasis, key=lambda x: quasis[x]))

    is_hidden_shift_identified = most_probable_after_m3 == hidden_shift_string
    if is_hidden_shift_identified:
        print("Most probable bitstring matches hidden shift 😊.")
    else:
        print("Most probable bitstring didn't match hidden shift ☹️.")
    print("Top 10 bitstrings and their quasi-probabilities:")
    topten = {
        unscramble(k): f"{v:.2e}"
        for k, v in sorted(quasis.items(), key=lambda x: x[1], reverse=True)[
            :10
        ]
    }
    max_probability_after_M3 = float(topten[most_probable_after_m3])
    display(topten)

    return max_probability_after_M3, is_hidden_shift_identified
print(f"Expected hidden shift string: {hidden_shift_string}")
max_probability_after_M3, is_hidden_shift_identified = perform_mitigation(
    mit, counts, qubit_mapping
)

Output:

Expected hidden shift string: 011010
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their quasi-probabilities:
{'011010': '1.01e+00',
 '001010': '8.75e-04',
 '001000': '7.38e-05',
 '010000': '4.51e-05',
 '111000': '2.18e-05',
 '001011': '1.74e-05',
 '000010': '6.42e-06',
 '011001': '-7.18e-06',
 '011000': '-4.53e-04',
 '010010': '-1.28e-03'}

Compare la identificación de la cadena de cambio oculta antes y después de aplicar una correccion M3

def compare_before_and_after_M3(
    max_probability_before_M3,
    max_probability_after_M3,
    is_hidden_shift_identified,
):
    is_probability_improved = (
        max_probability_after_M3 > max_probability_before_M3
    )
    print(f"Most probable probability before M3: {max_probability_before_M3}")
    print(f"Most probable probability after M3: {max_probability_after_M3}")
    if is_hidden_shift_identified and is_probability_improved:
        print("Readout error mitigation effective! 😊")
    else:
        print("Readout error mitigation not effective. ☹️")
compare_before_and_after_M3(
    max_probability_before_M3,
    max_probability_after_M3,
    is_hidden_shift_identified,
)

Output:

Most probable probability before M3: 0.9743
Most probable probability after M3: 1.01
Readout error mitigation effective! 😊

Representa gráficamente cómo varía el tiempo de CPU requerido por M3 en función del número de disparos

# Collect samples for numbers of shots varying from 5000 to 25000.
shots_range = range(5000, NUM_SHOTS + 1, 2500)
times = []
for shots in shots_range:
    print(f"Applying M3 correction to {shots} shots...")
    t0 = timeit.default_timer()
    _ = mit.apply_correction(
        pub_result.data.meas.slice_shots(range(shots)).get_counts(),
        qubit_mapping,
    )
    t1 = timeit.default_timer()
    print(f"\tDone in {t1 - t0} seconds.")
    times.append(t1 - t0)

fig, ax = plt.subplots()
ax.plot(shots_range, times, "o--")
ax.set_xlabel("Shots")
ax.set_ylabel("Time (s)")
ax.set_title("Time to apply M3 correction")

Output:

Applying M3 correction to 5000 shots...
	Done in 0.003321983851492405 seconds.
Applying M3 correction to 7500 shots...
	Done in 0.004425413906574249 seconds.
Applying M3 correction to 10000 shots...
	Done in 0.006366567220538855 seconds.
Applying M3 correction to 12500 shots...
	Done in 0.0071477219462394714 seconds.
Applying M3 correction to 15000 shots...
	Done in 0.00860048783943057 seconds.
Applying M3 correction to 17500 shots...
	Done in 0.010026784148067236 seconds.
Applying M3 correction to 20000 shots...
	Done in 0.011459112167358398 seconds.
Applying M3 correction to 22500 shots...
	Done in 0.012727141845971346 seconds.
Applying M3 correction to 25000 shots...
	Done in 0.01406092382967472 seconds.
Applying M3 correction to 27500 shots...
	Done in 0.01546052098274231 seconds.
Applying M3 correction to 30000 shots...
	Done in 0.016769016161561012 seconds.
Applying M3 correction to 32500 shots...
	Done in 0.019537431187927723 seconds.
Applying M3 correction to 35000 shots...
	Done in 0.019739801064133644 seconds.
Applying M3 correction to 37500 shots...
	Done in 0.021093040239065886 seconds.
Applying M3 correction to 40000 shots...
	Done in 0.022840639110654593 seconds.
Applying M3 correction to 42500 shots...
	Done in 0.023974396288394928 seconds.
Applying M3 correction to 45000 shots...
	Done in 0.026412792038172483 seconds.
Applying M3 correction to 47500 shots...
	Done in 0.026364430785179138 seconds.
Applying M3 correction to 50000 shots...
	Done in 0.02820305060595274 seconds.
Text(0.5, 1.0, 'Time to apply M3 correction')
Output of the previous code cell

Interpretación de la trama

El gráfico anterior muestra que el tiempo necesario para aplicar la corrección M3 aumenta linealmente con el número de disparos.


Escalado hacia arriba

n_qubits = 80
rng = Random(12345)
circuit, hidden_shift, hidden_shift_string = run_hidden_shift_circuit(
    n_qubits, rng
)

print(f"Hidden shift string {hidden_shift_string}")

Output:

Hidden shift string 00000010100110101011101110010001010000110011101001101010101001111001100110000111
isa_circuit = get_isa_circuit(circuit, backend)
job = run_sampler(backend, isa_circuit, NUM_SHOTS)
mit, qubit_mapping = setup_mthree_mitigation(isa_circuit, backend)
counts, pub_result = get_bitstring_counts(job)
probs, most_probable = find_hidden_shift_bitstring(
    counts, hidden_shift_string
)

Output:

Expected hidden shift string: 00000010100110101011101110010001010000110011101001101010101001111001100110000111
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their probabilities:
{'00000010100110101011101110010001010000110011101001101010101001111001100110000111': (0.50402,
  80),
 '00000010100110101011101110010001010000110011100001101010101001111001100110000111': (0.0396,
  79),
 '00000010100110101011101110010001010000110011101001101010101001111001100100000111': (0.0323,
  79),
 '00000010100110101011101110010001010000110011101001101010101001101001100110000111': (0.01936,
  79),
 '00000010100110101011101110010011010000110011101001101010101001111001100110000111': (0.01432,
  79),
 '00000010100110101011101110010001010000110011101001101010101001011001100110000111': (0.0101,
  79),
 '00000010100110101011101110010001010000110011101001101010101001110001100110000111': (0.00924,
  79),
 '00000010100110101011101110010001010000010011101001101010101001111001100110000111': (0.00908,
  79),
 '00000010100110101011100110010001010000110011101001101010101001111001100110000111': (0.00888,
  79),
 '00000010100110101011101110010001010000110011101001100010101001111001100110000111': (0.0082,
  79)}

Vemos que se ha encontrado la cadena de desplazamiento oculta correcta. Además, las nueve cadenas de bits más probables son erróneas sólo en una posición.

Anota la probabilidad más probable:

max_probability_before_M3 = probs[most_probable]
max_probability_before_M3

Output:

0.50402
print(f"Expected hidden shift string: {hidden_shift_string}")
max_probability_after_M3, is_hidden_shift_identified = perform_mitigation(
    mit, counts, qubit_mapping
)

Output:

Expected hidden shift string: 00000010100110101011101110010001010000110011101001101010101001111001100110000111
Most probable bitstring matches hidden shift 😊.
Top 10 bitstrings and their quasi-probabilities:
{'00000010100110101011101110010001010000110011101001101010101001111001100110000111': '9.85e-01',
 '00000010100110101011101110010001010000110011100001101010101001111001100110000111': '6.84e-03',
 '00000010100110101011100110010001010000110011101001101010101001111001100110000111': '3.87e-03',
 '00000010100110101011101110010011010000110011101001101010101001111001100110000111': '3.42e-03',
 '00000010100110101011101110010001010000110011101001101010101001111001100100000111': '3.30e-03',
 '00000010100110101011101110010001010000110011101001101010101001110001100110000111': '3.28e-03',
 '00000010100010101011101110010001010000110011101001101010101001111001100110000111': '2.62e-03',
 '00000010100110101011101110010001010000110011101001101010101001101001100110000111': '2.43e-03',
 '00000010100110101011101110010000010000110011101001101010101001111001100110000111': '1.73e-03',
 '00000010100110101011101110010001010000110011101001101010101001111001000110000111': '1.63e-03'}
compare_before_and_after_M3(
    max_probability_before_M3,
    max_probability_after_M3,
    is_hidden_shift_identified,
)

Output:

Most probable probability before M3: 0.54348
Most probable probability after M3: 0.99
Readout error mitigation effective! 😊

Los resultados muestran que el error de lectura fue la fuente dominante de error y que la mitigación M3 fue eficaz.

¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.