Spanish
Idiomas
English
Bengali
French
German
Japanese
Korean
Portuguese
Spanish
Tamil

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 StateFns, 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})
    ])
  ])
])

ListOps: 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 OperatorBases, 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 CircuitOps. 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 OperatorBases 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 de OperatorBases 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 elementos oplist. Para simplificar la transmisión, esta función se define sobre arreglos NumPy.

  • coeff - Un coeficiente que multiplica la primitiva. Ten en cuenta que coeff puede ser int, float, complex o un objeto Parameter libre (de qiskit.circuit en Terra) para ser vinculado más tarde usando my_op.bind_parameters.

  • abelian - Indica si se sabe que los operadores en oplist conmutan mutuamente (generalmente se establece después de ser transformado por el convertidor AbelianGrouper).

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 PauliOps.

[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 EvolvedOps 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

Expectations son conversores que permiten el cálculo de los valores esperados de observables. Recorren un árbol de operadores, reemplazando OperatorStateFns (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 CircuitStateFns a VectorStateFns 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 CircuitStateFns con el CircuitSampler

El CircuitSampler recorre un Operador y convierte cualquier CircuitStateFns 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 SoftwareVersion
qiskit-terra0.19.0.dev0+fe3eb3f
qiskit-aer0.10.0.dev0+b78f265
qiskit-ignis0.7.0.dev0+0eb9dcc
qiskit-ibmq-provider0.17.0.dev0+15d2dfe
System information
Python version3.9.5
Python compilerClang 10.0.0
Python builddefault, May 18 2021 12:31:01
OSDarwin
CPUs4
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.