Nota
Esta página fue generada a partir de tutorials/operators/01_operator_flow.ipynb.
Flujo del Operador¶
Introducción¶
Qiskit proporciona clases que representan estados, operadores, sumas, productos tensoriales y composiciones de los mismos. Estas construcciones algebraicas nos permiten construir expresiones que representan operadores.
Introducimos expresiones creándolas a partir de operadores de Pauli. En secciones posteriores exploraremos operadores y estados con mayor detalle, cómo están representados, y qué podemos hacer con ellos. En la última sección construiremos un estado, lo desarrollaremos con un Hamiltoniano, y calcularemos los valores esperados de un observable.
Operadores de Pauli, sumas, composiciones y productos tensoriales¶
Los operadores base más importantes son los operadores de Pauli, los cuales son representados de esta manera.
[1]:
from qiskit.opflow import I, X, Y, Z
print(I, X, Y, Z)
I X Y Z
Estos operadores también pueden llevar un coeficiente.
[2]:
print(1.5 * I)
print(2.5 * X)
1.5 * I
2.5 * X
Estos coeficientes permiten que los operadores se utilicen como términos en una suma.
[3]:
print(X + 2.0 * Y)
1.0 * X
+ 2.0 * Y
Los productos tensoriales se denotan con un caret (acento circunflejo), como este.
[4]:
print(X^Y^Z)
XYZ
La composición es denotada por el símbolo @
.
[5]:
print(X @ Y @ Z)
iI
En los dos ejemplos anteriores, el producto tensorial y la composición de los operadores de Pauli se redujeron inmediatamente al operador de Pauli equivalente (posiblemente de múltiples qubits). Si aplicamos el producto tensorial o componemos objetos más complicados, el resultado son objetos que representan las operaciones no evaluadas. Es decir, expresiones algebraicas.
Por ejemplo, la composición de dos sumas
[6]:
print((X + Y) @ (Y + Z))
1j * Z
+ -1j * Y
+ 1.0 * I
+ 1j * X
Y el producto tensorial de dos sumas da
[7]:
print((X + Y) ^ (Y + Z))
1.0 * XY
+ 1.0 * XZ
+ 1.0 * YY
+ 1.0 * YZ
Echemos un vistazo a los tipos introducidos anteriormente. Primero a los operadores de Pauli.
[8]:
(I, X)
[8]:
(PauliOp(Pauli('I'), coeff=1.0), PauliOp(Pauli('X'), coeff=1.0))
Cada operador Pauli es una instancia de PauliOp
, que envuelve a una instancia de qiskit.quantum_info.Pauli
, y añade un coeficiente coeff
. En general, un PauliOp
representa un producto tensorial ponderado de operadores Pauli.
[9]:
2.0 * X^Y^Z
[9]:
PauliOp(Pauli('XYZ'), coeff=2.0)
Para la codificación de los operadores de Pauli como pares de valores booleanos, ve la documentación de qiskit.quantum_info.Pauli
.
Todos los objetos que representan operadores, ya sea una «primitiva» como PauliOp
, o expresiones algebraicas llevan un coeficiente
[10]:
print(1.1 * ((1.2 * X)^(Y + (1.3 * Z))))
1.2 * (
1.1 * XY
+ 1.4300000000000002 * XZ
)
A continuación, echamos un vistazo más amplio y profundo a los operadores, estados y los componentes básicos de los algoritmos cuánticos de Qiskit.
Parte I: Funciones de Estado y Mediciones¶
Los estados cuánticos están representados por subclases de la clase StateFn
. Hay cuatro representaciones de estados cuánticos: DictStateFn
es una representación escasa en la base computacional, respaldada por un dict
. VectorStateFn
es una representación densa en la base computacional respaldada por un arreglo numpy. CircuitStateFn
está respaldado por un circuito y representa el estado obtenido al ejecutar el circuito en el estado con todos sus qubits en cero en la base computacional. OperatorStateFn
representa estados mixtos a través de una matriz de densidad. (Como veremos más adelante, OperatorStateFn
también se usa para representar observables)
Se proporcionan varias instancias de StateFn
para mayor comodidad. Por ejemplo Zero, One, Plus, Minus
.
[11]:
from qiskit.opflow import (StateFn, Zero, One, Plus, Minus, H,
DictStateFn, VectorStateFn, CircuitStateFn, OperatorStateFn)
Zero
y One
representan los estados cuánticos \(|0\rangle\) y \(|1\rangle\). Están representados a través de DictStateFn
.
[12]:
print(Zero, One)
DictStateFn({'0': 1}) DictStateFn({'1': 1})
Plus
y Minus
, representan los estados \((|0\rangle + |1\rangle)/\sqrt{2}\) y \((|0\rangle - |1\rangle)/\sqrt{2}\), son representados a través de circuitos. H
es un sinónimo para Plus
.
[13]:
print(Plus, Minus)
CircuitStateFn(
┌───┐
q: ┤ H ├
└───┘
) CircuitStateFn(
┌───┐┌───┐
q: ┤ X ├┤ H ├
└───┘└───┘
)
La indexación en estados cuánticos se realiza con el método eval
. Estos ejemplos devuelven los coeficientes de los estados base 0
y 1
. (Abajo, podemos ver que el método eval
también se utiliza para realizar otros cálculos.)
[14]:
print(Zero.eval('0'))
print(Zero.eval('1'))
print(One.eval('1'))
print(Plus.eval('0'))
print(Minus.eval('1'))
1.0
0.0
1.0
(0.7071067811865475+0j)
(-0.7071067811865475+8.7e-17j)
El vector dual de un estado cuántico, que es el bra correspondiente de un ket se obtiene mediante el método adjoint
. El StateFn
lleva una bandera is_measurement
, que es False
si el objeto es un ket y True
si es un bra.
Aquí, construimos \(\langle 1 |\).
[15]:
One.adjoint()
[15]:
DictStateFn({'1': 1}, coeff=1.0, is_measurement=True)
Por comodidad, se puede obtener el vector dual con una tilde, de la siguiente manera
[16]:
~One
[16]:
DictStateFn({'1': 1}, coeff=1.0, is_measurement=True)
Operaciones algebraicas y predicados¶
Se admiten muchas operaciones algebraicas y predicados entre StateFn
s, que incluyen:
+
- adición-
- resta, negación (multiplicación escalar por -1)*
- multiplicación escalar/
- división escalar@
- composición^
- producto tensorial o potencia tensorial (producto tensorial consigo mismo n veces)**
- potencia de composición (componer consigo mismo n veces)==
- igualdad~
- adjunto, alternando entre una Función de Estado y Medición
Ten en cuenta que estos operadores obedecen las reglas de Python de precedencia de operadores, que puede que no sea lo que esperas matemáticamente. Por ejemplo, I^X + X^I
en realidad se analizará como I ^ (X + X) ^ I == 2 * (I^X^I)
porque Python evalúa +
antes de ^
. En estos casos, puedes utilizar los métodos (.tensor()
, etc) o paréntesis.
StateFn
lleva un coeficiente. Esto nos permite multiplicar estados por un escalar, y de esta forma la posibilidad de construir sumas.
Aquí construimos \((2 + 3i)|0\rangle\).
[17]:
(2.0 + 3.0j) * Zero
[17]:
DictStateFn({'0': 1}, coeff=(2+3j), is_measurement=False)
Aquí vemos que al añadir dos DictStateFn
se devuelve un objeto del mismo tipo. Construimos \(|0\rangle + |1\rangle\).
[18]:
print(Zero + One)
DictStateFn({'0': 1.0, '1': 1.0})
Ten en cuenta que debes normalizar los estados a mano. Por ejemplo, para construir \((|0\rangle + |1\rangle)/\sqrt{2}\) escribimos
[19]:
import math
v_zero_one = (Zero + One) / math.sqrt(2)
print(v_zero_one)
DictStateFn({'0': 1.0, '1': 1.0}) * 0.7071067811865475
En otros casos, el resultado es una representación simbólica de una suma. Por ejemplo, aquí hay una representación de \(|+\rangle + |-\rangle\).
[20]:
print(Plus + Minus)
SummedOp([
CircuitStateFn(
┌───┐
q: ┤ H ├
└───┘
),
CircuitStateFn(
┌───┐┌───┐
q: ┤ X ├┤ H ├
└───┘└───┘
)
])
El operador de composición se utiliza para realizar un producto interno, que de forma predeterminada se mantiene en una forma sin evaluar. Aquí hay una representación de \(\langle 1 | 1 \rangle\).
[21]:
print(~One @ One)
ComposedOp([
DictMeasurement({'1': 1}),
DictStateFn({'1': 1})
])
Ten en cuenta que la bandera is_measurement
causa que el estado (bra) ~One
sea impreso DictMeasurement
.
Las expresiones simbólicas pueden ser evaluadas con el método eval
.
[22]:
(~One @ One).eval()
[22]:
1.0
[23]:
(~v_zero_one @ v_zero_one).eval()
[23]:
0.9999999999999998
Aquí se muestra \(\langle - | 1 \rangle = \langle (\langle 0| - \langle 1|)/\sqrt{2} | 1\rangle\).
[24]:
(~Minus @ One).eval()
[24]:
(-0.7071067811865475-8.7e-17j)
El operador de composición @
es equivalente a llamar al método compose
.
[25]:
print((~One).compose(One))
ComposedOp([
DictMeasurement({'1': 1}),
DictStateFn({'1': 1})
])
Los productos internos también pueden ser evaluados usando el método eval
directamente, sin necesidad de construir un ComposedOp
.
[26]:
(~One).eval(One)
[26]:
1.0
Los productos tensoriales simbólicos se construyen de la siguiente manera. Aquí se muestra \(|0\rangle \otimes |+\rangle\).
[27]:
print(Zero^Plus)
TensoredOp([
DictStateFn({'0': 1}),
CircuitStateFn(
┌───┐
q: ┤ H ├
└───┘
)
])
Esto puede ser representado como un CircuitStateFn
simple (no compuesto).
[28]:
print((Zero^Plus).to_circuit_op())
CircuitStateFn(
┌───┐
q_0: ┤ H ├
└───┘
q_1: ─────
)
Las potencias tensoriales se construyen utilizando el caret (acento circunflejo) ^
de la siguiente manera. Aquí se muestran \(600 (|11111\rangle + |00000\rangle)\), y \(|10\rangle^{\otimes 3}\).
[29]:
print(600 * ((One^5) + (Zero^5)))
print((One^Zero)^3)
DictStateFn({'11111': 1.0, '00000': 1.0}) * 600.0
DictStateFn({'101010': 1})
El método to_matrix_op
convierte a VectorStateFn
.
[30]:
print(((Plus^Minus)^2).to_matrix_op())
print(((Plus^One)^2).to_circuit_op())
print(((Plus^One)^2).to_matrix_op().sample())
VectorStateFn(Statevector([ 0.25-6.1e-17j, -0.25+6.1e-17j, 0.25-6.1e-17j,
-0.25+6.1e-17j, -0.25+6.1e-17j, 0.25-6.1e-17j,
-0.25+6.1e-17j, 0.25-6.1e-17j, 0.25-6.1e-17j,
-0.25+6.1e-17j, 0.25-6.1e-17j, -0.25+6.1e-17j,
-0.25+6.1e-17j, 0.25-6.1e-17j, -0.25+6.1e-17j,
0.25-6.1e-17j],
dims=(2, 2, 2, 2)))
CircuitStateFn(
┌───┐
q_0: ┤ X ├
├───┤
q_1: ┤ H ├
├───┤
q_2: ┤ X ├
├───┤
q_3: ┤ H ├
└───┘
)
{'1101': 0.2734375, '0111': 0.2509765625, '1111': 0.24609375, '0101': 0.2294921875}
Construir un StateFn es sencillo. La clase StateFn
también sirve como una fábrica, y puede tomar cualquier primitiva aplicable en su constructor y devolver la subclase StateFn correcta. Actualmente, se pueden pasar las siguientes primitivas al constructor, listadas junto a la subclase StateFn
producen:
str (igual a alguna base de cadena de bits) -> DictStateFn
dict -> DictStateFn
Objeto de resultado de Qiskit -> DictStateFn
list-> VectorStateFn
np.ndarray -> VectorStateFn
Statevector -> VectorStateFn
QuantumCircuit-> CircuitStateFn
Instruction -> CircuitStateFn
OperatorBase -> OperatorStateFn
[31]:
print(StateFn({'0':1}))
print(StateFn({'0':1}) == Zero)
print(StateFn([0,1,1,0]))
from qiskit.circuit.library import RealAmplitudes
print(StateFn(RealAmplitudes(2)))
DictStateFn({'0': 1})
True
VectorStateFn(Statevector([0.+0.j, 1.+0.j, 1.+0.j, 0.+0.j],
dims=(2, 2)))
CircuitStateFn(
┌──────────────────────────────────────────────────────────┐
q_0: ┤0 ├
│ RealAmplitudes(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5],θ[6],θ[7]) │
q_1: ┤1 ├
└──────────────────────────────────────────────────────────┘
)
Parte II: PrimitiveOp
¶
Los operadores básicos son subclases de PrimitiveOp
. Al igual que StateFn
, PrimitiveOp
también es una fábrica para crear el tipo correcto de PrimitiveOp
para una primitiva dada. Actualmente, las siguientes primitivas pueden pasarse al constructor, listadas junto a la subclase PrimitiveOp
producen:
Pauli de Terra -> PauliOp
Instruction -> CircuitOp
QuantumCircuit -> CircuitOp
Lista 2d -> MatrixOp
np.ndarray -> MatrixOp
spmatrix -> MatrixOp
quantum_info.Operator de Terra -> MatrixOp
[32]:
from qiskit.opflow import X, Y, Z, I, CX, T, H, S, PrimitiveOp
Elementos de matriz¶
El método eval
devuelve una columna de un operador. Por ejemplo, el operador Pauli \(X\) está representado por un PauliOp
. Al solicitar una columna, se devuelve una instancia de la representación recortada, un DictStateFn
.
[33]:
X
[33]:
PauliOp(Pauli('X'), coeff=1.0)
[34]:
print(X.eval('0'))
DictStateFn({'1': (1+0j)})
De ello se deduce que la indexación en un operador, es decir, la obtención de un elemento de matriz, se realiza con dos llamadas al método eval
.
Tenemos \(X = \left(\begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right)\). Y el elemento de matriz \(\left\{X \right\}_{0,1}\) es
[35]:
X.eval('0').eval('1')
[35]:
(1+0j)
Aquí se muestra un ejemplo usando el operador de dos qubits CX
, que es el operador X
controlado, y su representación en un circuito.
[36]:
print(CX)
print(CX.to_matrix().real) # The imaginary part vanishes.
q_0: ──■──
┌─┴─┐
q_1: ┤ X ├
└───┘
[[1. 0. 0. 0.]
[0. 0. 0. 1.]
[0. 0. 1. 0.]
[0. 1. 0. 0.]]
[37]:
CX.eval('01') # 01 is the one in decimal. We get the first column.
[37]:
VectorStateFn(Statevector([0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
dims=(2, 2)), coeff=1.0, is_measurement=False)
[38]:
CX.eval('01').eval('11') # This returns element with (zero-based) index (1, 3)
[38]:
(1+0j)
Aplicar un operador a un vector de estado¶
La aplicación de un operador a un vector de estado puede hacerse con el método compose
(o equivalentemente, el operador @
). Aquí está una representación de \(X | 1 \rangle = |0\rangle\).
[39]:
print(X @ One)
ComposedOp([
X,
DictStateFn({'1': 1})
])
Una representación más simple, la representación DictStateFn
de \(|0\rangle\), se obtiene con eval
.
[40]:
(X @ One).eval()
[40]:
DictStateFn({'0': (1+0j)}, coeff=(1+0j), is_measurement=False)
El paso intermedio ComposedOp
puede evitarse si se utiliza eval
directamente.
[41]:
X.eval(One)
[41]:
DictStateFn({'0': (1+0j)}, coeff=(1+0j), is_measurement=False)
Los productos tensoriales y de composición de los operadores se aplican con @
y ^
. Aquí se muestran algunos ejemplos.
[42]:
print(((~One^2) @ (CX.eval('01'))).eval())
print(((H^5) @ ((CX^2)^I) @ (I^(CX^2)))**2)
print((((H^5) @ ((CX^2)^I) @ (I^(CX^2)))**2) @ (Minus^5))
print(((H^I^I)@(X^I^I)@Zero))
(1+0j)
┌───┐ ┌───┐
q_0: ──■──┤ H ├───────■──┤ H ├─────
┌─┴─┐└───┘┌───┐┌─┴─┐└───┘┌───┐
q_1: ┤ X ├──■──┤ H ├┤ X ├──■──┤ H ├
└───┘┌─┴─┐├───┤└───┘┌─┴─┐├───┤
q_2: ──■──┤ X ├┤ H ├──■──┤ X ├┤ H ├
┌─┴─┐└───┘├───┤┌─┴─┐└───┘├───┤
q_3: ┤ X ├──■──┤ H ├┤ X ├──■──┤ H ├
└───┘┌─┴─┐├───┤└───┘┌─┴─┐├───┤
q_4: ─────┤ X ├┤ H ├─────┤ X ├┤ H ├
└───┘└───┘ └───┘└───┘
CircuitStateFn(
┌───┐┌───┐ ┌───┐ ┌───┐
q_0: ┤ X ├┤ H ├──■──┤ H ├───────■──┤ H ├─────
├───┤├───┤┌─┴─┐└───┘┌───┐┌─┴─┐└───┘┌───┐
q_1: ┤ X ├┤ H ├┤ X ├──■──┤ H ├┤ X ├──■──┤ H ├
├───┤├───┤└───┘┌─┴─┐├───┤└───┘┌─┴─┐├───┤
q_2: ┤ X ├┤ H ├──■──┤ X ├┤ H ├──■──┤ X ├┤ H ├
├───┤├───┤┌─┴─┐└───┘├───┤┌─┴─┐└───┘├───┤
q_3: ┤ X ├┤ H ├┤ X ├──■──┤ H ├┤ X ├──■──┤ H ├
├───┤├───┤└───┘┌─┴─┐├───┤└───┘┌─┴─┐├───┤
q_4: ┤ X ├┤ H ├─────┤ X ├┤ H ├─────┤ X ├┤ H ├
└───┘└───┘ └───┘└───┘ └───┘└───┘
)
CircuitStateFn(
┌─────────────┐
q_0: ┤0 ├─────
│ │
q_1: ┤1 Pauli(XII) ├─────
│ │┌───┐
q_2: ┤2 ├┤ H ├
└─────────────┘└───┘
)
[43]:
print(~One @ Minus)
ComposedOp([
DictMeasurement({'1': 1}),
CircuitStateFn(
┌───┐┌───┐
q: ┤ X ├┤ H ├
└───┘└───┘
)
])
Parte III: ListOp
y subclases¶
ListOp
¶
ListOp
es un contenedor para vectorizar operaciones de manera efectiva sobre una lista de operadores y estados.
[44]:
from qiskit.opflow import ListOp
print((~ListOp([One, Zero]) @ ListOp([One, Zero])))
ComposedOp([
ListOp([
DictMeasurement({'1': 1}),
DictMeasurement({'0': 1})
]),
ListOp([
DictStateFn({'1': 1}),
DictStateFn({'0': 1})
])
])
Por ejemplo, la composición anterior se distribuye sobre las listas (ListOp
) utilizando el método de simplificación reduce
.
[45]:
print((~ListOp([One, Zero]) @ ListOp([One, Zero])).reduce())
ListOp([
ListOp([
ComposedOp([
DictMeasurement({'1': 1}),
DictStateFn({'1': 1})
]),
ComposedOp([
DictMeasurement({'1': 1}),
DictStateFn({'0': 1})
])
]),
ListOp([
ComposedOp([
DictMeasurement({'0': 1}),
DictStateFn({'1': 1})
]),
ComposedOp([
DictMeasurement({'0': 1}),
DictStateFn({'0': 1})
])
])
])
ListOp
s: SummedOp
, ComposedOp
, TensoredOp
¶
ListOp
, presentado anteriormente, es útil para operaciones de vectorización. Pero también sirve como la superclase para clases compuestas tipo lista. Si ya has jugado con lo anterior, notarás que puedes ejecutar operaciones fácilmente entre OperatorBase
s, que puede que no sepamos como ejecutarlo de manera óptima en general (o simplemente no se haya implementado un procedimiento eficiente por el momento), tales como la adición entre CircuitOp
s. En esos casos, puedes recibir un resultado ListOp
(o una subclase) de tu operación que representa la ejecución diferida de la operación. Por ejemplo, si intentas agregar un DictStateFn
y un CircuitStateFn
, recibirás un SummedOp
que representa la suma de los dos. Esta función de estado compuesto todavía tiene un eval
que funciona (pero es posible que deba realizar un cálculo no escalable tras bambalinas, como convertir ambos en vectores).
Estos OperatorBase
s compuestos son la forma en que construimos computación cada vez más compleja y rica a partir de los bloques de construcción PrimitiveOp
y StateFn
.
Cada ListOp
tiene cuatro propiedades:
oplist
- La lista deOperatorBase
s que pueden representar términos, factores, etc.combo_fn
- La función toma una lista de números complejos a un valor de salida que define cómo combinar las salidas de los elementosoplist
. Para simplificar la transmisión, esta función se define sobre arreglos NumPy.coeff
- Un coeficiente que multiplica la primitiva. Ten en cuenta quecoeff
puede ser int, float, complex o un objetoParameter
libre (deqiskit.circuit
en Terra) para ser vinculado más tarde usandomy_op.bind_parameters
.abelian
- Indica si se sabe que los operadores enoplist
conmutan mutuamente (generalmente se establece después de ser transformado por el convertidorAbelianGrouper
).
Ten en cuenta que ListOp
soporta sobrecargas de secuencia típicas, así que puedes usar indexación como my_op[4]
para acceder a la OperatorBase
en oplist
.
OperatorStateFn
¶
Se ha mencionado anteriormente que OperatorStateFn
representa un operador de densidad. Pero, si la bandera is_measurement
es True
, entonces OperatorStateFn
representa un observable. El valor esperado de este observable puede construirse a través de ComposedOp
. O, directamente, utilizando eval
. Recuerda que la bandera (propiedad) is_measurement
se establece mediante el método adjoint
.
Aquí construimos el observable correspondiente al operador de Pauli \(Z\). Ten en cuenta que al imprimirlo, se le llama OperatorMeasurement
.
[46]:
print(StateFn(Z).adjoint())
StateFn(Z).adjoint()
OperatorMeasurement(Z)
[46]:
OperatorStateFn(PauliOp(Pauli('Z'), coeff=1.0), coeff=1.0, is_measurement=True)
Aquí calculamos \(\langle 0 | Z | 0 \rangle\), \(\langle 1 | Z | 1 \rangle\), y \(\langle + | Z | + \rangle\), donde \(|+\rangle = (|0\rangle + |1\rangle)/\sqrt{2}\).
[47]:
print(StateFn(Z).adjoint().eval(Zero))
print(StateFn(Z).adjoint().eval(One))
print(StateFn(Z).adjoint().eval(Plus))
(1+0j)
(-1+0j)
0j
Parte IV: Conversores¶
Los conversores son clases que manipulan operadores y estados y ejecutan bloques de construcción de algoritmos. Algunos ejemplos incluyen cambiar la base de operadores y Trotterización. Los conversores recorren una expresión y realizan una manipulación o sustitución particular, definida por el método convert()
del conversor, de los operadores internos. Normalmente, si un conversor encuentra un OperatorBase
en la recursión que es irrelevante para su propósito de conversión, ese OperatorBase
se deja sin cambios.
[48]:
import numpy as np
from qiskit.opflow import I, X, Y, Z, H, CX, Zero, ListOp, PauliExpectation, PauliTrotterEvolution, CircuitSampler, MatrixEvolution, Suzuki
from qiskit.circuit import Parameter
from qiskit import Aer
Evoluciones, exp_i()
, y EvolvedOp
¶
Cada PrimitiveOp
y ListOp
tiene una función .exp_i()
tal que H.exp_i()
corresponde a \(e^{-iH}\). En la práctica, sólo unos pocos de estos operadores tienen una exponenciación computable eficiente (como por ejemplo MatrixOp y los PauliOps con solo un qubit que no sea identidad de Pauli), así que necesitamos devolver un marcador de posición o una representación simbólica, (similar a cómo SummedOp
es un marcador de posición cuando no podemos realizar una suma). Este marcador de posición se llama EvolvedOp
, y contiene la OperatorBase
para ser exponenciada en su propiedad .primitive
.
Los operadores de Qiskit soportan completamente la parametrización, así que podemos usar un Parameter
para nuestro tiempo de evolución aquí. Ten en cuenta que no hay ningún argumento de «tiempo de evolución» en ninguna función. El flujo del Operator exponencia cualquier operador que se le indique, y si optamos por multiplicar el operador por un tiempo de evolución, \(e^{-iHt}\), se reflejará en nuestros parámetros de exponenciación.
Suma ponderada de los operadores de Pauli¶
Un Hamiltoniano expresado como una combinación lineal de operadores de Pauli multi-qubit puede ser construido de este modo.
[49]:
two_qubit_H2 = (-1.0523732 * I^I) + \
(0.39793742 * I^Z) + \
(-0.3979374 * Z^I) + \
(-0.0112801 * Z^Z) + \
(0.18093119 * X^X)
Nota que two_qubit_H2
está representado como un SummedOp
cuyos términos son PauliOp
s.
[50]:
print(two_qubit_H2)
-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX
A continuación, multiplicamos al Hamiltoniano por un Parameter
. Este Parameter
se almacena en la propiedad coeff
de SummedOp
. Llamando a exp_i()
, en el resultado; lo envuelve en EvolvedOp
, representando la exponenciación.
[51]:
evo_time = Parameter('θ')
evolution_op = (evo_time*two_qubit_H2).exp_i()
print(evolution_op) # Note, EvolvedOps print as exponentiations
print(repr(evolution_op))
e^(-i*1.0*θ * (
-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX
))
EvolvedOp(PauliSumOp(SparsePauliOp(['II', 'IZ', 'ZI', 'ZZ', 'XX'],
coeffs=[-1.0523732 +0.j, 0.39793742+0.j, -0.3979374 +0.j, -0.0112801 +0.j,
0.18093119+0.j]), coeff=1.0*θ), coeff=1.0)
Construimos h2_measurement
, que representa two_qubit_H2
como un observable.
[52]:
h2_measurement = StateFn(two_qubit_H2).adjoint()
print(h2_measurement)
OperatorMeasurement(-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX)
Construimos un estado de Bell \(|\Phi_+\rangle\) vía \(\text{CX} (H\otimes I) |00\rangle\).
[53]:
bell = CX @ (I ^ H) @ Zero
print(bell)
CircuitStateFn(
┌───┐
q_0: ┤ H ├──■──
└───┘┌─┴─┐
q_1: ─────┤ X ├
└───┘
)
Esta es la expresión \(H e^{-iHt} |\Phi_+\rangle\).
[54]:
evo_and_meas = h2_measurement @ evolution_op @ bell
print(evo_and_meas)
ComposedOp([
OperatorMeasurement(-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX),
e^(-i*1.0*θ * (
-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX
)),
CircuitStateFn(
┌───┐
q_0: ┤ H ├──■──
└───┘┌─┴─┐
q_1: ─────┤ X ├
└───┘
)
])
Normalmente, queremos aproximar \(e^{-iHt}\) usando compuertas de dos qubits. Esto se logra con el método convert
de PauliTrotterEvolution
, que recorre expresiones aplicando la trotterización a todos los EvolvedOp
s encontrados. Aunque utilizamos PauliTrotterEvolution
aquí, hay otras posibilidades, como MatrixEvolution
, que realiza la exponenciación exactamente.
[55]:
trotterized_op = PauliTrotterEvolution(trotter_mode=Suzuki(order=2, reps=1)).convert(evo_and_meas)
# We can also set trotter_mode='suzuki' or leave it empty to default to first order Trotterization.
print(trotterized_op)
ComposedOp([
OperatorMeasurement(-1.0523732 * II
+ 0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ
+ 0.18093119 * XX),
CircuitStateFn(
global phase: 1.0523732*θ
┌───┐ ┌───┐┌───┐┌──────────────────┐┌───┐┌───┐┌───┐»
q_0: ┤ H ├──■──┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├┤ X ├»
└───┘┌─┴─┐├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤└─┬─┘»
q_1: ─────┤ X ├┤ H ├──■────────────────────────■──┤ H ├──■──»
└───┘└───┘ └───┘ »
« ┌──────────────────┐┌───┐┌──────────────────┐┌──────────────────┐┌───┐»
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ Rz(0.39793742*θ) ├┤ Rz(0.39793742*θ) ├┤ X ├»
« └──────────────────┘└─┬─┘├──────────────────┤├──────────────────┤└─┬─┘»
«q_1: ──────────────────────■──┤ Rz(-0.3979374*θ) ├┤ Rz(-0.3979374*θ) ├──■──»
« └──────────────────┘└──────────────────┘ »
« ┌──────────────────┐┌───┐┌───┐┌───┐┌──────────────────┐┌───┐┌───┐
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├
« └──────────────────┘└─┬─┘├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤
«q_1: ──────────────────────■──┤ H ├──■────────────────────────■──┤ H ├
« └───┘ └───┘
)
])
trotterized_op
contiene un Parameter
. El método bind_parameters
recorre los valores de enlace de la expresión a los nombres de los parámetros mediante un dict
. En este caso, sólo hay un parámetro.
[56]:
bound = trotterized_op.bind_parameters({evo_time: .5})
bound
es un ComposedOp
. El segundo factor es el circuito. Dibujémoslo para verificar que se genera el enlace.
[57]:
bound[1].to_circuit().draw()
[57]:
global phase: 0.52619 ┌───┐ ┌───┐┌───┐┌─────────────────┐┌───┐┌───┐┌───┐┌─────────────────┐» q_0: ┤ H ├──■──┤ H ├┤ X ├┤ Rz(0.090465595) ├┤ X ├┤ H ├┤ X ├┤ Rz(-0.00564005) ├» └───┘┌─┴─┐├───┤└─┬─┘└─────────────────┘└─┬─┘├───┤└─┬─┘└─────────────────┘» q_1: ─────┤ X ├┤ H ├──■───────────────────────■──┤ H ├──■─────────────────────» └───┘└───┘ └───┘ » « ┌───┐┌────────────────┐┌────────────────┐┌───┐┌─────────────────┐┌───┐» «q_0: ┤ X ├┤ Rz(0.19896871) ├┤ Rz(0.19896871) ├┤ X ├┤ Rz(-0.00564005) ├┤ X ├» « └─┬─┘├────────────────┤├────────────────┤└─┬─┘└─────────────────┘└─┬─┘» «q_1: ──■──┤ Rz(-0.1989687) ├┤ Rz(-0.1989687) ├──■───────────────────────■──» « └────────────────┘└────────────────┘ » « ┌───┐┌───┐┌─────────────────┐┌───┐┌───┐ «q_0: ┤ H ├┤ X ├┤ Rz(0.090465595) ├┤ X ├┤ H ├ « ├───┤└─┬─┘└─────────────────┘└─┬─┘├───┤ «q_1: ┤ H ├──■───────────────────────■──┤ H ├ « └───┘ └───┘
Valor Esperado¶
Expectation
s son conversores que permiten el cálculo de los valores esperados de observables. Recorren un árbol de operadores, reemplazando OperatorStateFn
s (observables) con instrucciones equivalentes que son más susceptibles a la computación en hardware cuántico o clásico. Por ejemplo, si queremos medir el valor esperado de un Operador o
expresado como una suma de Paulis con respecto a alguna función de estado, pero solo puede acceder a mediciones diagonales en hardware cuántico, podemos crear un observable ~StateFn(o)
y usar una PauliExpectation
para convertirlo en una medición diagonal y pre-rotaciones de circuito para añadirlo al estado.
Otra Expectation
interesante es la AerPauliExpectation
, que convierte el observable en un CircuitStateFn
que contiene una instrucción especial de una instantánea de un valor esperado que Aer
puede ejecutar nativamente con alto rendimiento.
[58]:
# Note that XX was the only non-diagonal measurement in our H2 Observable
print(PauliExpectation(group_paulis=False).convert(h2_measurement))
SummedOp([
ComposedOp([
OperatorMeasurement(-1.0523732 * II),
II
]),
ComposedOp([
OperatorMeasurement(0.39793742 * IZ),
II
]),
ComposedOp([
OperatorMeasurement(-0.3979374 * ZI),
II
]),
ComposedOp([
OperatorMeasurement(-0.0112801 * ZZ),
II
]),
ComposedOp([
OperatorMeasurement(0.18093119 * ZZ),
┌───┐
q_0: ┤ H ├
├───┤
q_1: ┤ H ├
└───┘
])
])
De forma predeterminada group_paulis=True
, que utilizará el AbelianGrouper
para convertir el SummedOp
en grupos de Paulis que conmutan mutuamente a nivel de qubits. Esto reduce la sobrecarga de ejecución del circuito, ya que cada grupo puede compartir la misma ejecución del circuito.
[59]:
print(PauliExpectation().convert(h2_measurement))
SummedOp([
ComposedOp([
OperatorMeasurement(0.18093119 * ZZ
- 1.0523732 * II),
┌───┐
q_0: ┤ H ├
├───┤
q_1: ┤ H ├
└───┘
]),
ComposedOp([
OperatorMeasurement(0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ),
II
])
])
Ten en cuenta que los conversores actúan recursivamente, es decir, atraviesan una expresión aplicando su acción sólo cuando sea posible. Así que sólo podemos convertir nuestra evolución y expresión de medición. Podríamos equivalentemente haber compuesto el h2_measurement
convertido con nuestra evolución CircuitStateFn
. Procedemos aplicando la conversión en toda la expresión.
[60]:
diagonalized_meas_op = PauliExpectation().convert(trotterized_op)
print(diagonalized_meas_op)
SummedOp([
ComposedOp([
OperatorMeasurement(0.18093119 * ZZ
- 1.0523732 * II),
CircuitStateFn(
global phase: 1.0523732*θ
┌───┐ ┌───┐┌───┐┌──────────────────┐┌───┐┌───┐┌───┐»
q_0: ┤ H ├──■──┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├┤ X ├»
└───┘┌─┴─┐├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤└─┬─┘»
q_1: ─────┤ X ├┤ H ├──■────────────────────────■──┤ H ├──■──»
└───┘└───┘ └───┘ »
« ┌──────────────────┐┌───┐┌──────────────────┐┌──────────────────┐┌───┐»
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ Rz(0.39793742*θ) ├┤ Rz(0.39793742*θ) ├┤ X ├»
« └──────────────────┘└─┬─┘├──────────────────┤├──────────────────┤└─┬─┘»
«q_1: ──────────────────────■──┤ Rz(-0.3979374*θ) ├┤ Rz(-0.3979374*θ) ├──■──»
« └──────────────────┘└──────────────────┘ »
« ┌──────────────────┐┌───┐┌───┐┌───┐┌──────────────────┐┌───┐┌───┐┌───┐
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├┤ H ├
« └──────────────────┘└─┬─┘├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤├───┤
«q_1: ──────────────────────■──┤ H ├──■────────────────────────■──┤ H ├┤ H ├
« └───┘ └───┘└───┘
)
]),
ComposedOp([
OperatorMeasurement(0.39793742 * IZ
- 0.3979374 * ZI
- 0.0112801 * ZZ),
CircuitStateFn(
global phase: 1.0523732*θ
┌───┐ ┌───┐┌───┐┌──────────────────┐┌───┐┌───┐┌───┐»
q_0: ┤ H ├──■──┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├┤ X ├»
└───┘┌─┴─┐├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤└─┬─┘»
q_1: ─────┤ X ├┤ H ├──■────────────────────────■──┤ H ├──■──»
└───┘└───┘ └───┘ »
« ┌──────────────────┐┌───┐┌──────────────────┐┌──────────────────┐┌───┐»
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ Rz(0.39793742*θ) ├┤ Rz(0.39793742*θ) ├┤ X ├»
« └──────────────────┘└─┬─┘├──────────────────┤├──────────────────┤└─┬─┘»
«q_1: ──────────────────────■──┤ Rz(-0.3979374*θ) ├┤ Rz(-0.3979374*θ) ├──■──»
« └──────────────────┘└──────────────────┘ »
« ┌──────────────────┐┌───┐┌───┐┌───┐┌──────────────────┐┌───┐┌───┐
«q_0: ┤ Rz(-0.0112801*θ) ├┤ X ├┤ H ├┤ X ├┤ Rz(0.18093119*θ) ├┤ X ├┤ H ├
« └──────────────────┘└─┬─┘├───┤└─┬─┘└──────────────────┘└─┬─┘├───┤
«q_1: ──────────────────────■──┤ H ├──■────────────────────────■──┤ H ├
« └───┘ └───┘
)
])
])
Ahora vinculamos múltiples valores de parámetro en un ListOp
, seguido por eval
para evaluar toda la expresión. Podríamos haber usado eval
antes si hubiéramos vinculado previamente, pero no sería eficiente. Aquí, eval
convertirá nuestro CircuitStateFn
s a VectorStateFn
s a través de la simulación internamente.
[61]:
evo_time_points = list(range(8))
h2_trotter_expectations = diagonalized_meas_op.bind_parameters({evo_time: evo_time_points})
Estos son los valores esperados \(\langle \Phi_+| e^{iHt} H e^{-iHt} |\Phi_+\rangle\) correspondientes a los diferentes valores del parámetro.
[62]:
h2_trotter_expectations.eval()
[62]:
array([-0.88272211+0.0e+00j, -0.88272211+0.0e+00j, -0.88272211+0.0e+00j,
-0.88272211+0.0e+00j, -0.88272211+0.0e+00j, -0.88272211+0.0e+00j,
-0.88272211+5.6e-17j, -0.88272211+0.0e+00j])
Ejecutar CircuitStateFn
s con el CircuitSampler
¶
El CircuitSampler
recorre un Operador y convierte cualquier CircuitStateFn
s en aproximaciones de la función de estado resultante por un DictStateFn
o VectorStateFn
usando un backend cuántico. Ten en cuenta que para aproximar el valor del CircuitStateFn
, se debe 1) enviar la función de estado a través de un canal despolarizante, que destruirá toda la información de la fase y 2) reemplazar las frecuencias muestreadas con raíces cuadradas de la frecuencia, en lugar de la probabilidad cruda de muestreo (que sería el equivalente a muestrear el cuadrado de la función de estado, por la regla de Born).
[63]:
sampler = CircuitSampler(backend=Aer.get_backend('aer_simulator'))
# sampler.quantum_instance.run_config.shots = 1000
sampled_trotter_exp_op = sampler.convert(h2_trotter_expectations)
sampled_trotter_energies = sampled_trotter_exp_op.eval()
print('Sampled Trotterized energies:\n {}'.format(np.real(sampled_trotter_energies)))
Sampled Trotterized energies:
[-0.88272211 -0.88272211 -0.88272211 -0.88272211 -0.88272211 -0.88272211
-0.88272211 -0.88272211]
Ten en cuenta, de nuevo; que los circuitos son reemplazados por dicts con raíces cuadradas de las probabilidades de muestreo del circuito. Echa un vistazo a una subexpresión antes y después de la conversión:
[64]:
print('Before:\n')
print(h2_trotter_expectations.reduce()[0][0])
print('\nAfter:\n')
print(sampled_trotter_exp_op[0][0])
Before:
ComposedOp([
OperatorMeasurement(0.18093119 * ZZ
- 1.0523732 * II),
CircuitStateFn(
┌───┐ ┌───┐┌───┐┌───────┐┌───┐┌───┐┌───┐┌───────┐┌───┐┌───────┐»
q_0: ┤ H ├──■──┤ H ├┤ X ├┤ Rz(0) ├┤ X ├┤ H ├┤ X ├┤ Rz(0) ├┤ X ├┤ Rz(0) ├»
└───┘┌─┴─┐├───┤└─┬─┘└───────┘└─┬─┘├───┤└─┬─┘└───────┘└─┬─┘├───────┤»
q_1: ─────┤ X ├┤ H ├──■─────────────■──┤ H ├──■─────────────■──┤ Rz(0) ├»
└───┘└───┘ └───┘ └───────┘»
« ┌───────┐┌───┐┌───────┐┌───┐┌───┐┌───┐┌───────┐┌───┐┌───┐┌───┐
«q_0: ┤ Rz(0) ├┤ X ├┤ Rz(0) ├┤ X ├┤ H ├┤ X ├┤ Rz(0) ├┤ X ├┤ H ├┤ H ├
« ├───────┤└─┬─┘└───────┘└─┬─┘├───┤└─┬─┘└───────┘└─┬─┘├───┤├───┤
«q_1: ┤ Rz(0) ├──■─────────────■──┤ H ├──■─────────────■──┤ H ├┤ H ├
« └───────┘ └───┘ └───┘└───┘
)
])
After:
ComposedOp([
OperatorMeasurement(0.18093119 * ZZ
- 1.0523732 * II),
DictStateFn({'00': 0.7167090588237321, '11': 0.6973723001381686})
])
[65]:
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.19.0.dev0+fe3eb3f |
qiskit-aer | 0.10.0.dev0+b78f265 |
qiskit-ignis | 0.7.0.dev0+0eb9dcc |
qiskit-ibmq-provider | 0.17.0.dev0+15d2dfe |
System information | |
Python version | 3.9.5 |
Python compiler | Clang 10.0.0 |
Python build | default, May 18 2021 12:31:01 |
OS | Darwin |
CPUs | 4 |
Memory (Gb) | 32.0 |
Fri Oct 29 16:10:45 2021 BST |
This code is a part of Qiskit
© Copyright IBM 2017, 2021.
This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.