Simulación clásica de circuitos con OBP
En esta guía aprenderás a simular de forma clásica instancias de QuantumCircuit para estimar valores esperados únicamente mediante OBP.
Dado que OBP toma un observable y lo retropropaga a través de un circuito dado, la "simulación" de un circuito equivale a calcular el valor esperado del observable objetivo con respecto a ese circuito.
Como verás más adelante, el paquete qiskit-addon-obp incluso es capaz de manejar modelos de ruido simples, ¡lo que te permite calcular también valores esperados con ruido!
Construcción de un circuito de ejemplo
Para los fines de esta guía, usaremos el mismo circuito de ejemplo que en la guía de truncamiento de términos de Pauli:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
Sin embargo, lo anterior es únicamente el circuito que describe la evolución temporal bajo un hamiltoniano elegido. También necesitamos un estado inicial del que partir, con respecto al cual calculamos los valores esperados de nuestro observable.
Por supuesto, podríamos elegir el estado de todo ceros (o vacío) como estado inicial, pero para mostrar cómo se insertaría un estado inicial propio, elegimos uno diferente a continuación.
Una posibilidad sería anteponer el estado inicial a nuestro circuito de evolución temporal anterior: circuit.compose(initial_state, front=True).
Pero como ya hemos dividido en fragmentos nuestro circuit, es más fácil simplemente insertar el estado inicial como primer fragmento, que es lo que hacemos a continuación.
De este modo, podemos simplemente reemplazar el primer fragmento por otro estado inicial, si queremos cambiarlo en el futuro, sin tener que recalcular nuestros fragmentos.
from qiskit.circuit import QuantumCircuit
initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)
slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Simulación de un valor esperado sin ruido
Como observable objetivo, elegimos el observable ZZ sobre los qubits centrales:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
En este punto, ya estamos listos para simular clásicamente el valor esperado usando OBP.
Para ello, simplemente proporcionamos todos los fragmentos al método backpropagate, así:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Ahora hemos retropropagado nuestro observable objetivo obs a través del circuito completo (incluyendo el initial_state que colocamos en slices[0]), lo que resulta en un nuevo SparsePauliOp cuyo valor esperado obtenemos proyectándolo sobre el estado vacío (|00...00>).
Esto se puede lograr de forma directa sumando los coeficientes de todos los términos de Pauli definidos en la base computacional:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Como comprobación de validez (y para demostrar que esto funciona) podemos comparar nuestro resultado con el Statevector de Qiskit:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Algunas notas sobre el rendimiento
La eficiencia computacional de la llamada a backpropagate anterior dependerá en gran medida de muchos factores, entre ellos:
- la estructura del
circuit - el método de división del circuito en fragmentos
- el observable objetivo
- los parámetros de truncamiento
Dado que el método backpropagate simplifica el observable después de aplicar cada fragmento, el número de puertas en un fragmento puede influir drásticamente en la carga computacional.
La estrategia más agresiva en términos de simplificación del operador se consigue dividiendo el circuito en fragmentos de puertas individuales.
Además, puedes aprovechar todos los mecanismos de truncamiento integrados en el método backpropagate.
No lo hicimos arriba, lo que da como resultado un valor esperado exacto, pero puedes aprender cómo hacerlo en la guía de truncamiento de términos de Pauli.
Simulación de un valor esperado con ruido
El paquete qiskit-addon-obp también admite el manejo de modelos de ruido en forma de PauliLindbladErrors.
Esto es especialmente útil cuando has caracterizado el modelo de ruido de las capas de 2 qubits de tu circuito, por ejemplo usando el NoiseLearner.
En esta sección verás cómo puedes usar los objetos LayerError devueltos por el NoiseLearner para calcular valores esperados con ruido usando OBP.
Obtención de un modelo de ruido
Normalmente, ejecutarías el NoiseLearner para obtener un modelo de ruido de tu circuito concreto.
Para evitar complejidad (y aleatoriedad) en este tutorial, no lo haremos y en su lugar codificaremos directamente un modelo de ruido para nuestro circuito a continuación.
Sin embargo, nos aseguramos de que la estructura de nuestros datos coincida con la del NoiseLearnerResult.
En su forma actual (sin transpilar), nuestro circuito contiene 4 capas únicas de puertas de 2 qubits:
slices[1]: que tiene puertasRxxactuando sobre todos los pares impares de qubitsslices[2]: que tiene puertasRxxactuando sobre todos los pares pares de qubitsslices[3]: que tiene puertasRyyactuando sobre todos los pares impares de qubitsslices[4]: que tiene puertasRyyactuando sobre todos los pares pares de qubits
En la celda siguiente, construimos manualmente 4 instancias de LayerError, una para cada una de estas capas, con algunas tasas de error aleatorias.
import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)
# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on
np.random.seed(42)
layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
Si hubieras usado el NoiseLearner para identificar las capas únicas de puertas de 2 qubits de tu circuito y caracterizar su ruido, obtendrías un objeto NoiseLearnerResult.
Este resultado contendría una lista de objetos LayerError, igual que los que hemos construido manualmente arriba.
Para cada capa única de 2 qubits, el LayerError contiene:
- el
QuantumCircuitque representa esa capa de puertas de 2 qubits - los índices de los qubits sobre los que actúa este circuito
- el
PauliLindbladErrorque representa el modelo de ruido caracterizado de esta capa
El PauliLindbladError contendrá dos objetos:
- la lista de errores de Pauli que se han caracterizado
- las tasas de error correspondientes a cada uno de esos errores de Pauli
Normalmente, la lista de errores de Pauli será dispersa. Más concretamente, contendrá los errores de Pauli de un solo qubit en todos los qubits sobre los que actúan puertas, así como los errores de Pauli de dos qubits en todos aquellos qubits que están conectados.
Inserción de las capas con ruido en nuestro circuito
En la sección anterior, hemos construido específicamente un LayerError para cada una de nuestras capas únicas de puertas de 2 qubits conocidas.
Esto significa que sabemos qué LayerError coincide exactamente con cada uno de nuestros fragmentos.
Normalmente, al usar el LayerError, tendrás que averiguar cuál es la capa única de puertas de 2 qubits y dónde aparece dentro de tu circuito.
Después tendrás que ajustar tu circuit y/o slices para insertar el LayerError en consecuencia.
Cómo hacerlo en el caso general queda fuera del alcance de esta guía práctica.
TODO: enlazar a documentación externa, una vez que exista.
Aquí, nuestra tarea es más sencilla porque sabemos a qué fragmento corresponde cada LayerError.
Por lo tanto, solo se trata de insertar nuevos fragmentos que representen el ruido.
Ten en cuenta que debemos envolver cada PauliLindbladError de LayerError.error en un PauliLindbladErrorInstruction para que sea una instrucción válida de QuantumCircuit.
from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction
noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)
Podemos comprobar nuestro trabajo y dibujar el circuito a continuación:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Simulación de un valor esperado con ruido
En este punto, la simulación clásica del valor esperado funciona exactamente igual que antes, solo que
vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)
Señalamos de nuevo que deben tenerse en cuenta varias consideraciones de rendimiento. Vuelve a la sección correspondiente más arriba.
