Skip to main contentIBM Quantum Documentation Preview

The Operator class

This page shows how to use the Operator class. For a high-level overview of operator representations in Qiskit, including the Operator class and others, see Overview of operator classes.

[1] :
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import CXGate, RXGate, XGate
from qiskit.quantum_info import Operator, Pauli, process_fidelity

Convert classes to Operators

Several other classes in Qiskit can be directly converted to an Operator object using the operator initialization method. For example:

  • Pauli objects
  • Gate and Instruction objects
  • QuantumCircuit objects

Note that the last point means you can use the Operator class as a unitary simulator to compute the final unitary matrix for a quantum circuit, without having to call a simulator backend. If the circuit contains any unsupported operations, an exception is raised. Unsupported operations are: measure, reset, conditional operations, or a gate that does not have a matrix definition or decomposition in terms of gate with matrix definitions.

[2] :
# Create an Operator from a Pauli object
 
pauliXX = Pauli("XX")
Operator(pauliXX)

Output:

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))
[3] :
# Create an Operator for a Gate object
Operator(CXGate())

Output:

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))
[4] :
# Create an operator from a parameterized Gate object
Operator(RXGate(np.pi / 2))

Output:

Operator([[0.70710678+0.j        , 0.        -0.70710678j],
          [0.        -0.70710678j, 0.70710678+0.j        ]],
         input_dims=(2,), output_dims=(2,))
[5] :
# 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)

Output:

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))

Use Operators in circuits

Unitary Operators can be directly inserted into a QuantumCircuit using the QuantumCircuit.append method. This converts the Operator into a UnitaryGate object, which is added to the circuit.

If the operator is not unitary, an exception is raised. This can be checked using the Operator.is_unitary() function, which returns True if the operator is unitary and False otherwise.

[6] :
# Create an operator
XX = Operator(Pauli("XX"))
 
# Add to a circuit
circ = QuantumCircuit(2, 2)
circ.append(XX, [0, 1])
circ.measure([0, 1], [0, 1])
circ.draw("mpl")

Output:

Note that in the above example the operator is initialized from a Pauli object. However, the Pauli object may also be directly inserted into the circuit itself and will be converted into a sequence of single-qubit Pauli gates:

[7] :
# Add to a circuit
circ2 = QuantumCircuit(2, 2)
circ2.append(Pauli("XX"), [0, 1])
circ2.measure([0, 1], [0, 1])
circ2.draw()

Output:

     ┌────────────┐┌─┐   
q_0: ┤0           ├┤M├───
     │  Pauli(XX) │└╥┘┌─┐
q_1: ┤1           ├─╫─┤M├
     └────────────┘ ║ └╥┘
c: 2/═══════════════╩══╩═
                    0  1 

Combine Operators

Operators may be combined using several methods.

Tensor product

Two operators AA and BB can be combined into a tensor product operator ABA\otimes B using the Operator.tensor function. Note that if both AA and BB are single-qubit operators, then A.tensor(B) = ABA\otimes B will have the subsystems indexed as matrix BB on subsystem 0, and matrix AA on subsystem 1.

[8] :
A = Operator(Pauli("X"))
B = Operator(Pauli("Z"))
A.tensor(B)

Output:

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))

Tensor expansion

A closely related operation is Operator.expand, which acts like a tensor product but in the reverse order. Hence, for two operators AA and BB you have A.expand(B) = BAB\otimes A where the subsystems are indexed as matrix AA on subsystem 0, and matrix BB on subsystem 1.

[9] :
A = Operator(Pauli("X"))
B = Operator(Pauli("Z"))
A.expand(B)

Output:

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

You can also compose two operators AA and BB to implement matrix multiplication using the Operator.compose method. A.compose(B) returns the operator with matrix B.AB.A:

[10] :
A = Operator(Pauli("X"))
B = Operator(Pauli("Z"))
A.compose(B)

Output:

Operator([[ 0.+0.j,  1.+0.j],
          [-1.+0.j,  0.+0.j]],
         input_dims=(2,), output_dims=(2,))

You can also compose in the reverse order by applying BB in front of AA using the front kwarg of compose: A.compose(B, front=True) = A.BA.B:

[11] :
A = Operator(Pauli("X"))
B = Operator(Pauli("Z"))
A.compose(B, front=True)

Output:

Operator([[ 0.+0.j, -1.+0.j],
          [ 1.+0.j,  0.+0.j]],
         input_dims=(2,), output_dims=(2,))

Subsystem composition

Note that the previous compose requires that the total output dimension of the first operator AA is equal to the total input dimension of the composed operator BB (and similarly, the output dimension of BB must be equal to the input dimension of AA when composing with front=True).

You can also compose a smaller operator with a selection of subsystems on a larger operator using the qargs kwarg of compose, either with or without front=True. In this case, the relevant input and output dimensions of the subsystems being composed must match. Note that the smaller operator must always be the argument of the compose method.

For example, to compose a two-qubit gate with a three-qubit operator:

[12] :
# Compose XZ with a 3-qubit identity operator
op = Operator(np.eye(2**3))
XZ = Operator(Pauli("XZ"))
op.compose(XZ, qargs=[0, 2])

Output:

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))
[13] :
# Compose YX in front of the previous operator
op = Operator(np.eye(2**3))
YX = Operator(Pauli("YX"))
op.compose(YX, qargs=[0, 2], front=True)

Output:

Operator([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j, 0.+0.j, 0.+0.j],
          [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.-1.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.-1.j],
          [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j, 0.+0.j],
          [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
          [0.+1.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.+1.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
          [0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]],
         input_dims=(2, 2, 2), output_dims=(2, 2, 2))

Linear combinations

Operators may also be combined using standard linear operators for addition, subtraction, and scalar multiplication by complex numbers.

[14] :
XX = Operator(Pauli("XX"))
YY = Operator(Pauli("YY"))
ZZ = Operator(Pauli("ZZ"))
 
op = 0.5 * (XX + YY - 3 * ZZ)
op

Output:

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))

An important point is that while tensor, expand, and compose preserves the unitarity of unitary operators, linear combinations do not; hence, adding two unitary operators will, in general, result in a non-unitary operator:

[15] :
op.is_unitary()

Output:

False

Implicit conversion to Operators

Note that for all the following methods, if the second object is not already an Operator object, it is implicitly converted into one by the method. This means that matrices can be passed in directly without being explicitly converted to an Operator first. If the conversion is not possible, an exception is raised.

[16] :
# Compose with a matrix passed as a list
Operator(np.eye(2)).compose([[0, 1], [1, 0]])

Output:

Operator([[0.+0.j, 1.+0.j],
          [1.+0.j, 0.+0.j]],
         input_dims=(2,), output_dims=(2,))

Compare Operators

Operators implement an equality method that can be used to check if two operators are approximately equal.

[17] :
Operator(Pauli("X")) == Operator(XGate())

Output:

True

Note that this checks that each matrix element of the operators is approximately equal; two unitaries that differ by a global phase are not considered equal:

[18] :
Operator(XGate()) == np.exp(1j * 0.5) * Operator(XGate())

Output:

False

Process fidelity

You can also compare operators using the process_fidelity function from the Quantum Information module. This is an information-theoretic quantity for how close two quantum channels are to each other, and in the case of unitary operators it does not depend on global phase.

[19] :
# 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)

Output:

Process fidelity = 1.0

Note that process fidelity is generally only a valid measure of closeness if the input operators are unitary (or CP in the case of quantum channels), and an exception is raised if the inputs are not CP.


Next steps

Recommendations