Saltar al contenido principal

Avance clásico (feedforward) y flujo de control

Versiones de paquetes

El código de esta página se desarrolló utilizando los siguientes requisitos. Recomendamos usar estas versiones o más recientes.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
Circuitos dinámicos ahora disponibles en todos los backends

La nueva versión de los circuitos dinámicos está ahora disponible para todos los usuarios en todos los backends. Ahora puedes ejecutar circuitos dinámicos a escala de utilidad (utility scale). Consulta el anuncio para obtener más detalles.

Los circuitos dinámicos son herramientas poderosas con las que puedes medir qubits en la mitad de la ejecución de un circuito cuántico y luego realizar operaciones lógicas clásicas dentro del circuito, basadas en el resultado de esas mediciones intermedias del circuito (mid-circuit measurements). Este proceso también se conoce como avance clásico (classical feedforward). Si bien aún nos encontramos en una etapa temprana para entender cómo conseguir la mejor manera de aprovechar los circuitos dinámicos, la comunidad de investigación cuántica ya ha identificado una serie de casos de uso (use cases), tales como los siguientes:

Estas mejoras introducidas por los circuitos dinámicos, sin embargo, vienen con ciertas compensaciones (trade-offs). Las mediciones intermedias del circuito y las operaciones lógicas clásicas típicamente tienen tiempos de ejecución más largos que las puertas de dos qubits, y este incremento de tiempo podría anular los beneficios de reducir la profundidad del circuito. Por tanto, disminuir la longitud o tiempo de las mediciones de circuito intermedio, se ha posicionado como un área de enfoque para las mejoras a medida que IBM Quantum® lanza la nueva versión de circuitos dinámicos.

La especificación de OpenQASM 3 define varias estructuras de flujo de control, pero Qiskit Runtime actualmente solo admite la declaración condicional if. En el SDK de Qiskit, esto corresponde al método if_test en QuantumCircuit. Este método devuelve un gestor de contexto (context manager) y normalmente se usa en una declaración with. Esta guía describe cómo usar esta declaración condicional.

nota

Los ejemplos de código en esta guía utilizan la instrucción de medición estándar para mediciones en medio del circuito. Sin embargo, se recomienda que en su lugar utilices la instrucción MidCircuitMeasure, si el backend la admite. Consulta la documentación sobre Mediciones a la mitad del circuito para obtener más detalles.

Declaración if

La declaración if se utiliza para realizar operaciones de manera condicional basándose en el valor de un bit o registro clásico.

En el siguiente ejemplo, aplicamos una puerta de Hadamard a un qubit y lo medimos. Si el resultado es 1, entonces aplicamos una puerta X en el qubit, lo cual tiene el efecto de invertirlo (flipping it) de regreso al estado 0. Luego medimos el qubit nuevamente. El resultado de medición (resulting measurement outcome) debería ser 0 con una probabilidad del 100%.

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister

qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits

circuit.h(q0)
# Use MidCircuitMeasure() if it's supported by the backend.
# circuit.append(MidCircuitMeasure(), [q0], [c0])
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)):
circuit.x(q0)
circuit.measure(q0, c0)
circuit.draw("mpl")

# example output counts: {'0': 1024}

Output of the previous code cell

A la declaración with se le puede dar un objetivo de asignación que es en sí mismo un gestor de contexto (context manager) que puede ser almacenado y posteriormente utilizado para crear un bloque else, el cual se ejecuta siempre que los contenidos del bloque if no se ejecutan.

En el ejemplo a continuación, inicializamos registros con dos qubits y dos bits clásicos. Aplicamos una puerta de Hadamard al primer qubit y lo medimos. Si el resultado es 1, entonces aplicamos una puerta de Hadamard sobre el segundo qubit; de no ser así (o caso contrario, otherwise), aplicamos una puerta X sobre el segundo qubit. Finalmente medimos también el segundo qubit.

qubits = QuantumRegister(2)
clbits = ClassicalRegister(2)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1) = qubits
(c0, c1) = clbits

circuit.h(q0)
circuit.measure(q0, c0)
with circuit.if_test((c0, 1)) as else_:
circuit.h(q1)
with else_:
circuit.x(q1)
circuit.measure(q1, c1)

circuit.draw("mpl")

# example output counts: {'01': 260, '11': 272, '10': 492}

Output of the previous code cell

Además del condicionamiento sobre un solo bit clásico, también es posible condicionar sobre el valor de un registro clásico compuesto por múltiples bits.

En el ejemplo a continuación, aplicamos puertas de Hadamard a dos qubits y los medimos. Si el resultado es 01, es decir, el primer qubit es 1 y el segundo qubit es 0, entonces aplicamos una puerta X a un tercer qubit. Finalmente, medimos el tercer qubit. Ten en cuenta que, para mayor claridad, elegimos especificar el estado del tercer bit clásico, que es 0, en la condición if. En el dibujo del circuito, la condición se indica mediante los círculos sobre los bits clásicos que están siendo condicionados. Un círculo negro indica condicionamiento para 1, mientras que un círculo blanco indica condicionamiento para 0.

qubits = QuantumRegister(3)
clbits = ClassicalRegister(3)
circuit = QuantumCircuit(qubits, clbits)
(q0, q1, q2) = qubits
(c0, c1, c2) = clbits

circuit.h([q0, q1])
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.if_test((clbits, 0b001)):
circuit.x(q2)
circuit.measure(q2, c2)

circuit.draw("mpl")

# example output counts: {'101': 269, '011': 260, '000': 252, '010': 243}

Output of the previous code cell

Expresiones clásicas

El módulo de expresiones clásicas de Qiskit qiskit.circuit.classical contiene una representación de exploración de las operaciones de ejecución (runtime operations) en valores clásicos durante la ejecución de circuitos. Debido a limitaciones de hardware, actualmente solo se admiten condiciones en QuantumCircuit.if_test().

El siguiente ejemplo muestra que puedes usar el cálculo de la paridad para crear un estado GHZ de n-qubits usando circuitos dinámicos. Primero, se generan n/2n/2 pares de Bell en los qubits adyacentes. Luego, se unen (o enlazan) estos pares mediante el uso de una capa de puertas CNOT entre ellos. A continuación, mides el qubit objetivo (target qubit) de todos los CNOT anteriores y reinicias cada qubit medido al estado 0\vert 0 \rangle. Aplicas la operación XX a cada sitio no medido para el cual la paridad de todos los bits de medición precedentes sea impar. Finalmente, se aplican puertas CNOT a los qubits medidos para restablecer el entrelazamiento (entanglement) perdido en la medición.

En el cálculo de paridad, el primer elemento de la expresión construida involucra la elevación (lifting) del objeto de Python mr[0] a un nodo Value (lift se utiliza para convertir objetos arbitrarios en expresiones clásicas). Esto no es necesario para mr[1] y el posible registro clásico posterior, ya que son entradas para expr.bit_xor, y cualquier elevación necesaria se realiza automáticamente en estos casos. Tales expresiones se pueden construir en bucles y otras construcciones (constructs).

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr

num_qubits = 8
if num_qubits % 2 or num_qubits < 4:
raise ValueError("num_qubits must be an even integer ≥ 4")
meas_qubits = list(range(2, num_qubits, 2)) # qubits to measure and reset

qr = QuantumRegister(num_qubits, "qr")
mr = ClassicalRegister(len(meas_qubits), "m")
qc = QuantumCircuit(qr, mr)

# Create local Bell pairs
qc.reset(qr)
qc.h(qr[::2])
for ctrl in range(0, num_qubits, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])

# Glue neighboring pairs
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])

# Measure boundary qubits between pairs,reset to 0
for k, q in enumerate(meas_qubits):
qc.measure(qr[q], mr[k])
qc.reset(qr[q])

# Parity-conditioned X corrections
# Each non-measured qubit gets flipped iff the parity (XOR) of all
# preceding measurement bits is 1
for tgt in range(num_qubits):
if tgt in meas_qubits: # skip measured qubits
continue
# all measurement registers whose physical qubit index < tgt
left_bits = [k for k, q in enumerate(meas_qubits) if q < tgt]
if not left_bits: # skip if list empty
continue

# build XOR-parity expression
parity = expr.lift(
mr[left_bits[0]]
) # lift the first bit to Value so it will be treated like a boolean.
for k in left_bits[1:]:
parity = expr.bit_xor(
mr[k], parity
) # calculate parity with all other bits
with qc.if_test(parity): # Add X if parity is 1
qc.x(qr[tgt])

# Re-entangle measured qubits
for ctrl in range(1, num_qubits - 1, 2):
qc.cx(qr[ctrl], qr[ctrl + 1])
qc.draw(output="mpl", style="iqp", idle_wires=False, fold=-1)

Output of the previous code cell

Encontrar backends que admiten circuitos dinámicos

Para encontrar todos los backends a los que tu cuenta puede acceder y que admiten circuitos dinámicos, ejecuta un código como el siguiente. Este ejemplo asume que has guardado tus credenciales de inicio de sesión. También podrías especificar explícitamente las credenciales al inicializar tu cuenta de servicio de Qiskit Runtime. Esto te permitiría ver los backends disponibles en una instancia específica o tipo de plan, por ejemplo.

Notas
  • Los backends que están disponibles para la cuenta dependen de la instancia especificada en las credenciales.
  • La nueva versión de los circuitos dinámicos está ahora disponible para todos los usuarios en todos los backends. Consulta el anuncio para obtener más detalles.
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
dc_backends = service.backends(dynamic_circuits=True)
print(dc_backends)
[<IBMBackend('ibm_pittsburgh')>, <IBMBackend('ibm_boston')>, <IBMBackend('ibm_fez')>, <IBMBackend('ibm_miami')>, <IBMBackend('ibm_marrakesh')>, <IBMBackend('ibm_torino')>, <IBMBackend('ibm_kingston')>]

Limitaciones de Qiskit Runtime

Ten en cuenta las siguientes restricciones al ejecutar circuitos dinámicos en Qiskit Runtime.

  • Debido a la memoria física limitada en la electrónica de control, también hay un límite en la cantidad de declaraciones if y en el tamaño de sus operandos. Este límite es una función del número de transmisiones (broadcasts) y del número de bits transmitidos en un trabajo (no en un circuito).

    Al procesar una condición if, los datos de medición deben transferirse a la lógica de control para realizar esa evaluación. Una transmisión es una transferencia de datos clásicos únicos, y los bits transmitidos son el número de bits clásicos que se transfieren. Considera lo siguiente:

    c0 = ClassicalRegister(3)
    c1 = ClassicalRegister(5)
    ...
    with circuit.if_test((c0, 1)) ...
    with circuit.if_test((c0, 3)) ...
    with circuit.if_test((c1[2], 1)) ...

    En el ejemplo de código anterior, los dos primeros objetos if_test en c0 se consideran una sola transmisión porque el contenido de c0 no ha cambiado y, por lo tanto, no necesita volver a transmitirse. El if_test en c1 es una segunda transmisión. La primera transmite los tres bits en c0 y la segunda transmite solo un bit, lo que hace un total de cuatro bits transmitidos.

    Actualmente, si transmites 60 bits cada vez, el trabajo puede tener aproximadamente 300 transmisiones. Sin embargo, si transmites solo un bit cada vez, el trabajo puede tener 2400 transmisiones.

  • El operando utilizado en una instrucción if_test debe ser de 32 bits o menos. Por lo tanto, si estás comparando todo un ClassicalRegister, el tamaño de ese ClassicalRegister debe ser de 32 bits o menos. Sin embargo, si estás comparando solo un bit de un ClassicalRegister, ese ClassicalRegister puede ser de cualquier tamaño (dado que el operando es de solo un bit).

    Por ejemplo, el bloque de código "No válido" no funciona porque cr tiene más de 32 bits. Sin embargo, puedes usar un registro clásico con más de 32 bits si solo estás probando un bit, como se muestra en el bloque de código "Válido".

       cr = ClassicalRegister(50)
    qr = QuantumRegister(50)
    circuit = QuantumCircuit(qr, cr)
    ...
    circ.measure(qr, cr)
    with circ.if_test((cr, 15)):
    ...
  • No se permiten sentencias condicionales anidadas. Por ejemplo, el siguiente bloque de código no funcionará porque tiene una declaración if_test dentro de otro if_test:

       c1 = ClassicalRegister(1, "c1")
    c2 = ClassicalRegister(2, "c2")
    ...
    with circ.if_test((c1, 1)):
    with circ.if_test(c2, 1)):
    ...
  • No se admite el uso de un reset o mediciones dentro de instrucciones condicionales.

  • Las operaciones aritméticas no son admitidas.

  • Consulta la tabla de funciones de OpenQASM 3 para determinar qué propiedades y qué características (features) son compatibles con OpenQASM 3 en Qiskit Runtime y Qiskit.

  • Cuando se utiliza OpenQASM 3 (en vez de QuantumCircuit) como formato de entrada (input format) para pasar circuitos a las primitivas de Qiskit Runtime, solo se admiten las instrucciones que pueden cargarse en Qiskit. Las operaciones clásicas, por ejemplo, no son compatibles porque no se pueden cargar en Qiskit. Consulta Importar un programa OpenQASM 3 en Qiskit para obtener más información.

  • Las instrucciones for, while y switch no son compatibles (no están admitidas).

Usar circuitos dinámicos con Estimator

Dado que Estimator no admite circuitos dinámicos, puedes usar Sampler y construir tus propios circuitos de medición en su lugar. Alternativamente, puedes usar la primitiva Executor, que admite circuitos dinámicos.

Para replicar el comportamiento del Estimator, sigue este proceso:

  1. Agrupa los términos de todos los observables en una partición. Esto se puede hacer utilizando la API PauliList, por ejemplo.
    nota

    Puedes usar el atributo primitivo BitArray para calcular los valores esperados de los observables proporcionados.

  2. Ejecuta un circuito de cambio de base por partición (cualquier cambio de base que se deba hacer para cada partición). Consulta el módulo de utilidad complemento Measurement bases measurement_bases module para más información. Empieza a usar las utilidades.
  3. Vuelve a sumar los resultados de cada partición.

Siguientes pasos

Recomendaciones