Note
Cette page a été générée à partir de tutorials/circuits_advanced/2_operators_overview.ipynb.
Exécuter en mode interactif dans le IBM Quantum lab.
Opérateurs¶
[7]:
import numpy as np
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import execute, BasicAer
from qiskit.compiler import transpile
from qiskit.quantum_info.operators import Operator, Pauli
from qiskit.quantum_info import process_fidelity
from qiskit.extensions import RXGate, XGate, CXGate
La Classe Opérateurs¶
La classe Operator
est utilisée dans Qiskit pour représenter toute matrice unitaire agissant sur un système quantique. Elle possède plusieurs méthodes qui permettent de construire des opérateurs composés en utilisant les produits tensoriels avec de plus petits opérateurs, et pour composer des opérateurs.
Créer des Opérateurs¶
Le moyen le plus simple pour créer un opérateur est de l’initialiser avec une matrice construite par une liste ou un tableau Numpy. Par exemple, pour créer un opérateur Pauli-XX sur deux qubits :
[8]:
XX = Operator([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]])
XX
[8]:
Operator([[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
Propriétés de l’objet Operator¶
L’objet operator
contient la matrice ainsi que les dimensions d’entrée et de sortie d’un système ou d’un sous-système.
data
: Permet d’accéder à la matrice Numpy sous-jacente en utilisant la propriété Operator.data.dim
: Renvoie les dimensions d’entrée et de sortie de l’opérateur en utilisant la propriétéOperator.dim
. Note : la sortie est un couple `(input_dim, output_dim)`, qui correspond aux dimensions –en ordre inverse : colonnes,lignes– de la matrice sous-jacente.
[9]:
XX.data
[9]:
array([[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])
[10]:
input_dim, output_dim = XX.dim
input_dim, output_dim
[10]:
(4, 4)
Dimensions d’entrée et de sortie¶
La classe Operator
contient également les dimensions du sous-système qui peut être utile pour la composition d’opérateurs. Les valeurs de ces dimensions peuvent être obtenues en utilisant les fonctions input_dims
et output_dims
.
Pour des opérateurs de dimension \(2^N\) par \(2^M\), les dimensions d’entrée et de sortie seront automatiquement interprétées comme représentant M-qubit et N-qubit:
[11]:
op = Operator(np.random.rand(2 ** 1, 2 ** 2))
print('Input dimensions:', op.input_dims())
print('Output dimensions:', op.output_dims())
Input dimensions: (2, 2)
Output dimensions: (2,)
Si la matrice d’entrée ne peut être divisée en sous systèmes de qubits, elle sera stockée comme un opérateur sur un seul quibt. Par exemple pour une matrice de taille \(6\times6\) :
[12]:
op = Operator(np.random.rand(6, 6))
print('Input dimensions:', op.input_dims())
print('Output dimensions:', op.output_dims())
Input dimensions: (6,)
Output dimensions: (6,)
On peut aussi spécifier manuellement les dimensions d’entrée et de sortie au moment de l’initialisation d’un nouvel opérateur :
[13]:
# Force input dimension to be (4,) rather than (2, 2)
op = Operator(np.random.rand(2 ** 1, 2 ** 2), input_dims=[4])
print('Input dimensions:', op.input_dims())
print('Output dimensions:', op.output_dims())
Input dimensions: (4,)
Output dimensions: (2,)
[14]:
# Specify system is a qubit and qutrit
op = Operator(np.random.rand(6, 6),
input_dims=[2, 3], output_dims=[2, 3])
print('Input dimensions:', op.input_dims())
print('Output dimensions:', op.output_dims())
Input dimensions: (2, 3)
Output dimensions: (2, 3)
Les fonctions input_dims
et output_dims
permettent de connaître les dimensions d’entrée ou de sortie d’un sous ensemble du du système :
[15]:
print('Dimension of input system 0:', op.input_dims([0]))
print('Dimension of input system 1:', op.input_dims([1]))
Dimension of input system 0: (2,)
Dimension of input system 1: (3,)
Conversion de classes en opérateurs¶
Plusieurs autres types d’objets de Qiskit peuvent être convertis en objets de la classe ``Operator``en utilisant sa méthode d’initialisation. Par exemple :
les objets de type Pauli
les objets
Gate
etInstruction
ainsi que les objets
QuantumCircuits
Il est à noter que dans le dernier cas l’utilisation de la classe operator
permet de calculer la matrice unitaire correspondant à un circuit quantique sans avoir besoin d’utiliser un émulateur. Si le circuit contient une operation non supportée la fonction retournera une exception. Ces opérations sont : la mesure, la mise à zero, les opérations conditionnelles, ou toute autre porte qui n’est pas définie par une matrice unitaire ou qui ne peut pas être décomposée en portes définies par une matrice unitaire.
[16]:
# Create an Operator from a Pauli object
pauliXX = Pauli(label='XX')
Operator(pauliXX)
[16]:
Operator([[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
[19]:
# Create an Operator for a Gate object
Operator(CXGate())
[19]:
Operator([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
[20]:
# Create an operator from a parameterized Gate object
Operator(RXGate(np.pi / 2))
[20]:
Operator([[0.70710678+0.j , 0. -0.70710678j],
[0. -0.70710678j, 0.70710678+0.j ]],
input_dims=(2,), output_dims=(2,))
[21]:
# Create an operator from a QuantumCircuit object
circ = QuantumCircuit(10)
circ.h(0)
for j in range(1, 10):
circ.cx(j-1, j)
# Convert circuit to an operator by implicit unitary simulation
Operator(circ)
[21]:
Operator([[ 0.70710678+0.j, 0.70710678+0.j, 0. +0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j],
[ 0. +0.j, 0. +0.j, 0.70710678+0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j],
[ 0. +0.j, 0. +0.j, 0. +0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j],
...,
[ 0. +0.j, 0. +0.j, 0. +0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j],
[ 0. +0.j, 0. +0.j, 0.70710678+0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j],
[ 0.70710678+0.j, -0.70710678+0.j, 0. +0.j, ...,
0. +0.j, 0. +0.j, 0. +0.j]],
input_dims=(2, 2, 2, 2, 2, 2, 2, 2, 2, 2), output_dims=(2, 2, 2, 2, 2, 2, 2, 2, 2, 2))
Utilisation d’opérateurs dans les circuits¶
Les Operators
unitaires peuvent être directement ajoutés à un QuantumCircuit
en utilisant la méthode QuantumCircuit.append
. Ceci a pour effet de convertir l”Operator en objet de type UnitaryGate
et de l’ajouter au circuit.
Si l’opérateur n’est pas unitaire, une exception sera retournée à l’utilisateur. Ceci peut être prévenu par l’utilisation de la fonction Operator.is_unitary()
qui renvoie True
si l’opérateur est unitaire, et False
sinon.
[22]:
# Create an operator
XX = Operator(Pauli(label='XX'))
# Add to a circuit
circ = QuantumCircuit(2, 2)
circ.append(XX, [0, 1])
circ.measure([0,1], [0,1])
circ.draw('mpl')
[22]:

Notons que dans l’exemple ci-dessus, l’opérateur est initialisé à l’aide d’un objet de type Pauli
. Cependant l’objet de type Pauli
peut aussi être inséré directement dans le circuit, et sera converti en une séquence de gates de Pauli sur un seul qubit :
[23]:
backend = BasicAer.get_backend('qasm_simulator')
job = execute(circ, backend, basis_gates=['u1','u2','u3','cx'])
job.result().get_counts(0)
[23]:
{'11': 1024}
[24]:
# Add to a circuit
circ2 = QuantumCircuit(2, 2)
circ2.append(Pauli(label='XX'), [0, 1])
circ2.measure([0,1], [0,1])
circ2.draw()
[24]:
┌───────────┐┌─┐ q_0: ┤0 ├┤M├─── │ Pauli:XX │└╥┘┌─┐ q_1: ┤1 ├─╫─┤M├ └───────────┘ ║ └╥┘ c: 2/══════════════╩══╩═ 0 1
Combinaison d’opérateurs¶
Les opérateurs peuvent être combinés au moyen de différentes méthodes.
Produit Tensoriel¶
Deux opérateurs \(A\) et \(B\) peuvent être combinés en un opérateur qui est le produit tensoriel \(A\otimes B\) en utilisant la fonction Operator.tensor()
.
Si \(A\) et \(B\) sont des opérateurs chacun sur un seul qubit alors A.tensor(B)
= \(A\otimes B\) correspondra à l’application de la matrice \(B\) sur le sous système 0, et la matrice \(A\) sur le sous système 1.
[25]:
A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.tensor(B)
[25]:
Operator([[ 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[ 0.+0.j, -0.+0.j, 0.+0.j, -1.+0.j],
[ 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[ 0.+0.j, -1.+0.j, 0.+0.j, -0.+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
Expansion Tensorielle¶
Une opération très proche de la précédente est Operator.expand
qui agit de la même manière que le produit tensoriel, mais dans l’ordre inverse. C’est à dire que pour deux opérateurs \(A\) et \(B\) l’expansion A.expand(B) = \(B\otimes A\) correspondra à l’application de la matrice \(A\) sur le sous système 0, et la matrice \(B\) sur le sous système 1.
[26]:
A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.expand(B)
[26]:
Operator([[ 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[ 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, -0.+0.j, -1.+0.j],
[ 0.+0.j, 0.+0.j, -1.+0.j, -0.+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
Composition¶
Il est également possible de faire la composition de deux opérateurs \(A\) et \(B\) pour implémenter la multiplication de matrices en utilisant la méthode Operator.compose
. Ainsi, A.compose(B) retourne l’opérateur correspondant à la matrice \(B.A\) :
[27]:
A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.compose(B)
[27]:
Operator([[ 0.+0.j, 1.+0.j],
[-1.+0.j, 0.+0.j]],
input_dims=(2,), output_dims=(2,))
Il est également possible de faire la composition en ordre inverse en appliquant \(B\) avant \(A\), en utilisant l’argument de mot clef front de la méthode compose comme ceci : ``A.compose(B, front=True)``= \(A.B\) :
[28]:
A = Operator(Pauli(label='X'))
B = Operator(Pauli(label='Z'))
A.compose(B, front=True)
[28]:
Operator([[ 0.+0.j, -1.+0.j],
[ 1.+0.j, 0.+0.j]],
input_dims=(2,), output_dims=(2,))
Composition de sous-systèmes¶
On peut noter que dans les compositions vues précédemment, la dimension totale de sortie du premier opérateur \(A\) est égale à la dimension totale d’entrée de l’opérateur \(B\) (et de même, la dimension de sortie de \(B\) doit être égale à la dimension de l’entrée de \(A\) lorsque l’on utilise la composition avec l’option front=True
).
Il est également possible d’effectuer les compositions sur un sous système d’un opérateur plus grand en utilisant l’argument de mot clef qargs
(kwarq) de la méthode compose (avec ou sans l’utilisation de front=True
). Dans ce cas, les dimensions correspondant aux sous systèmes faisant l’objet de la composition doivent se correspondre. Note : l’opérateur du plus petit sous système doit être passé en argument de la méthode ``compose`` appliquée au système
Par exemple, pour composer une porte à deux qubits dans un opérateur à trois qubits :
[29]:
# Compose XZ with an 3-qubit identity operator
op = Operator(np.eye(2 ** 3))
XZ = Operator(Pauli(label='XZ'))
op.compose(XZ, qargs=[0, 2])
[29]:
Operator([[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, -1.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
-1.+0.j],
[ 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, -1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, -1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j]],
input_dims=(2, 2, 2), output_dims=(2, 2, 2))
[30]:
# Compose YX in front of the previous operator
op = Operator(np.eye(2 ** 3))
YX = Operator(Pauli(label='YX'))
op.compose(XZ, qargs=[0, 2], front=True)
[30]:
Operator([[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, -1.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
-1.+0.j],
[ 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, -1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j, -1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j]],
input_dims=(2, 2, 2), output_dims=(2, 2, 2))
Combinaisons linéaires¶
Les opérateurs peuvent aussi être combinés en utilisant les fonctions standards d’addition de soustraction et de multiplication scalaire par des nombres complexes.
[31]:
XX = Operator(Pauli(label='XX'))
YY = Operator(Pauli(label='YY'))
ZZ = Operator(Pauli(label='ZZ'))
op = 0.5 * (XX + YY - 3 * ZZ)
op
[31]:
Operator([[-1.5+0.j, 0. +0.j, 0. +0.j, 0. +0.j],
[ 0. +0.j, 1.5+0.j, 1. +0.j, 0. +0.j],
[ 0. +0.j, 1. +0.j, 1.5+0.j, 0. +0.j],
[ 0. +0.j, 0. +0.j, 0. +0.j, -1.5+0.j]],
input_dims=(2, 2), output_dims=(2, 2))
Il est important de noter que si les méthodes tensor
, expand
and compose
conservent la propriété d’unitarité des opérateurs, les combinaisons linéaires d’opérateurs par contre ne le feront pas; ainsi l’addition de deux opérateurs unitaires donnera en général un opérateur non unitaire :
[32]:
op.is_unitary()
[32]:
False
Conversion implicite en opérateur¶
Notez que pour les méthodes ci-dessous, si le second objet utilisé n’est pas déjà de classe Operator
, il sera implicitement converti en objet de classe Operator
.
Ceci signifie que l’on peut passer directement des objets de type matrice en argument, sans avoir à les convertir préalablement et explicitement en opérateur.
Si la conversion n’est pas possible, une exception sera retournée.
[33]:
# Compose with a matrix passed as a list
Operator(np.eye(2)).compose([[0, 1], [1, 0]])
[33]:
Operator([[0.+0.j, 1.+0.j],
[1.+0.j, 0.+0.j]],
input_dims=(2,), output_dims=(2,))
Comparaison d’opérateurs¶
Une méthode de test d’égalité est implémentée pour les objets de type Operator
, elle peut être utilisée pour vérifier si deux opérateurs sont approximativement égaux.
[34]:
Operator(Pauli(label='X')) == Operator(XGate())
[34]:
True
Notons que cette méthode vérifie l’égalité approximative entre les éléments correspondant des matrices représentant les opérateurs ; deux opérateurs unitaires qui diffèreraient par une phase globale ne seront pas considérés comme égaux :
[35]:
Operator(XGate()) == np.exp(1j * 0.5) * Operator(XGate())
[35]:
False
Fidélité de Process¶
Nous pouvons également comparer les opérateurs à l’aide de la fonction process_fidelity
du module Quantum Information. Il s’agit d’une notion de la théorie de l’information quantique permettant d’indiquer à quel point deux chemins quantiques sont équivalents, et dans le cas d’opérateurs unitaires, le résultat est indépendant de la phase globale.
[36]:
# Two operators which differ only by phase
op_a = Operator(XGate())
op_b = np.exp(1j * 0.5) * Operator(XGate())
# Compute process fidelity
F = process_fidelity(op_a, op_b)
print('Process fidelity =', F)
Process fidelity = 1.0
Note : la fidélité de process est en général une mesure valide de l’équivalence si les opérateurs considérés sont unitaires (ou CP- complètement positifs- dans le cas de chemins quantiques), et une exception sera retournée si les entrées ne sont pas CP.
[37]:
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
Version Information
Qiskit Software | Version |
---|---|
Qiskit | None |
Terra | 0.14.0 |
Aer | 0.6.0 |
Ignis | 0.3.0 |
Aqua | 0.7.0 |
IBM Q Provider | 0.6.1 |
System information | |
Python | 3.7.7 (default, Mar 26 2020, 10:32:53) [Clang 4.0.1 (tags/RELEASE_401/final)] |
OS | Darwin |
CPUs | 4 |
Memory (Gb) | 16.0 |
Wed Apr 29 12:36:22 2020 EDT |
This code is a part of Qiskit
© Copyright IBM 2017, 2020.
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.
[ ]: