# Operators

In this tutorial, we introduce 2 objects, Operator and PauliLabel, that represents operators in quantum mechanics. You may construct various physical observables with them. In QURI Parts, we mainly work with operators consists of Pauli strings.

## PauliLabel​

Pauli strings are ubiquitous in quantum computation. In QURI Parts, they are represented by PauliLabel. This section is devoted to explain what PauliLabel is made of and how to create them. We first start with their basic building block: Pauli matrices.

### Pauli matrices​

In QURI Parts, Pauli matrices are represented by an Enum: SinglePauli. They are not objects to be used for any computations directly, they are simply labels of what Pauli matrices a PauliLabel hold.

from quri_parts.core.operator import SinglePauliassert SinglePauli.X == 1assert SinglePauli.Y == 2assert SinglePauli.Z == 3

### Pauli strings​

As mentioned previously, Pauli strings are represented by PauliLabel. We introduce the interface and how to create one.

#### Interface​

In QURI Parts, a PauliLabel represents a Pauli string. It is a frozenset of tuple[qubit_index, SinglePauli].

class PauliLabel(frozenset(tuple[int, int])):    """First int represents the qubit index and    the second represents a SinglePauli    """    ...

#### Creating a PauliLabel​

There are various ways of creating a PauliLabel. Here, we introduce the simplest one, which is using the pauli_label function. For basic usage, the pauli_label function accepts 2 types of inputs:

1. A str that looks like a Pauli string.
2. Sequence of (qubit index, SinglePauli) pairs.
##### Create with a str​

We can create a PauliLabel by passing in a str that looks like a human-readable Pauli string

from quri_parts.core.operator import PauliLabel, pauli_labelprint("Create without spacing:", pauli_label("X0 Y1 Z2 Z3 X4 Y5"))print("Create with spacing:   ", pauli_label("X 0  Y 1  Z 2  Z 3  X 4  Y 5"))
#output    Create without spacing: X0 Y1 Z2 Z3 X4 Y5    Create with spacing:    X0 Y1 Z2 Z3 X4 Y5

Note that the order of Pauli matrices does not matter.

print("Create with X0 Y1 Z2:", pauli_label("X0 Y1 Z2"))print("Create with X0 Z2 Y1:", pauli_label("X0 Z2 Y1"))print(pauli_label("X0 Y1 Z2") == pauli_label("X0 Z2 Y1"))
#output    Create with X0 Y1 Z2: X0 Y1 Z2    Create with X0 Z2 Y1: X0 Y1 Z2    True
##### Create with a sequence of (qubit_index, SinglePauli)​

We can also create a PauliLabel by passing a sequence of (qubit_index, SinglePauli) into the pauli_label function.

print(pauli_label([(0, SinglePauli.X), (1, SinglePauli.Z)]))print(pauli_label(zip((0, 1), (SinglePauli.X, SinglePauli.Z))))
#output    X0 Z1    X0 Z1

There is a special PauliLabel: PAULI_IDENTITY. It represents the identity operator and is a PauliLabel with no entry.

from quri_parts.core.operator import PAULI_IDENTITY# PauliLabel() represents an empty frozenset.print(PauliLabel() == PAULI_IDENTITY)
#output    True

#### Methods PauliLabel provides​

The PauliLabel provides several methods that provides information about itself.

• index_and_pauli_id_list: A property that returns a tuple of (list[qubit index], list[SinglePauli])
pauli_label("X0 Y1 Z2").index_and_pauli_id_list
#output    ([0, 1, 2], [<SinglePauli.X: 1>, <SinglePauli.Y: 2>, <SinglePauli.Z: 3>])
• qubit_indices: The list of qubits this PauliLabel acts on.
pauli_label("X0 Y1 Z2").qubit_indices()
#output    [0, 1, 2]
• pauli_at: The Pauli matrix at the specified qubit. If the operator at the specified qubit index is identity, it returns None.
print(pauli_label("X0 Y1 Z2").pauli_at(0))print(pauli_label("X0 Y1 Z2").pauli_at(1))print(pauli_label("X0 Y1 Z2").pauli_at(2))print(pauli_label("X0 Y1 Z2").pauli_at(3))
#output    SinglePauli.X    SinglePauli.Y    SinglePauli.Z    None

## Operator​

Here, we introduce the Operator object. The Operator object represents a complex linear combination of PauliLabels.

### Interface​

In QURI Parts, it is implmented as a dictionary with PauliLabel as key and complex number as value. So, you can create an Operator with a dictionary.

from quri_parts.core.operator import Operatorop = Operator(    {        PAULI_IDENTITY: 8 + 1j,        pauli_label("X0 Y2"): -3    })print(op)
#output    (8+1j)*I + -3*X0 Y2

### Arithmetics with Operator​

You can add terms to an Operator with the add_term method, which updates the Operator in place. If a PauliLabel already exists in the Operator, it updates the coeffcient. Suppose the new coefficient is 0, the PauliLabel will be dropped from the Operator.

op = Operator(    {        PAULI_IDENTITY: 8 + 1j,        pauli_label("X0 Y2"): -3    })print(op, "\n")# Add a new term to the Operatorpl, coeff = pauli_label("Y0 Z3"), 10+1jprint(f"Add {coeff} * {pl}:")op.add_term(pl, coeff)print(op, "\n")# Add a PauliLabel that already exists in Operator to update the coefficientpl, coeff = PAULI_IDENTITY, -6print(f"Add {coeff} * {pl}:")op.add_term(pl, coeff)print(op, "\n")# Add a PauliLabel that already exists in Operator to cancel the termpl, coeff = pauli_label("X0 Y2"), 3print(f"Add {coeff} * {pl}:")op.add_term(pl, coeff)print(op)
#output    (8+1j)*I + -3*X0 Y2     Add (10+1j) * Y0 Z3:    (8+1j)*I + -3*X0 Y2 + (10+1j)*Y0 Z3     Add -6 * I:    (2+1j)*I + -3*X0 Y2 + (10+1j)*Y0 Z3     Add 3 * X0 Y2:    (2+1j)*I + (10+1j)*Y0 Z3

There are also 2 properties provided:

• n_terms: The number of terms in the Operator.
• constant: Returns the coefficient of PAULI_IDENTITY in the Operator. It gives 0 if PAULI_IDENTITY is not present.
op = Operator(    {        PAULI_IDENTITY: 8 + 1j,        pauli_label("X0 Y2"): -3    })print("n_terms:", op.n_terms)print("constant:", op.constant)
#output    n_terms: 2    constant: (8+1j)

The Operator object also provides several methods for basic arithmetics:

op1 =  Operator({pauli_label("X0 Z1"): 8j})op2 =  Operator({pauli_label("Y1"): -4})print("op1 = ", op1)print("op2 = ", op2)# Additionprint("")print("Addition:")print("op1 + op2", "=", op1 + op2)# Subtractionprint("")print("Subtraction:")print("op1 - op2", "=", op1 - op2)# Scalar Multiplicationprint("")print("Scalar Multiplication:")print("op1 * 3j", "=", op1 * 3j)# Scalar Divisionprint("")print("Scalar Division:")print("op1 / 2j", "=", op1 / 2j)# Operator Multiplicationprint("")print("Operator Multiplication:")print("op1 * op2", "=", op1 * op2)print("op2 * op1", "=", op2 * op1)# Hermitian conjugationprint("")print("Hermition Conjgation:")print("op1^†", "=", op1.hermitian_conjugated())print("op2^†", "=", op2.hermitian_conjugated())
#output    op1 =  8j*X0 Z1    op2 =  -4*Y1        Addition:    op1 + op2 = 8j*X0 Z1 + -4*Y1        Subtraction:    op1 - op2 = 8j*X0 Z1 + 4*Y1        Scalar Multiplication:    op1 * 3j = (-24+0j)*X0 Z1        Scalar Division:    op1 / 2j = (4+0j)*X0 Z1        Operator Multiplication:    op1 * op2 = (-32+0j)*X0 X1    op2 * op1 = (32+0j)*X0 X1        Hermition Conjgation:    op1^† = -8j*X0 Z1    op2^† = -4*Y1

There is also a special Operator: zero() that represents a zero operator. It is an Operator created with an empty dictionary.

from quri_parts.core.operator import zerozero_operator = zero()print(zero_operator == Operator())
#output    True

## Helper functions​

There are also various other helper functions that provides several arithmetical functionalities for Operator. Here we introduce:

• is_hermitian
• commutator
• truncate
• is_ops_close
• get_sparse_matrix

We provide examples for them below:

### is_hermitian​

is_hermitian checks if an Operator is hermitian or not.

from quri_parts.core.operator import is_hermitianop = Operator({pauli_label("X0"): 1})print(f"{op} is hermitian:", is_hermitian(op))op = Operator({pauli_label("X0"): 1j})print(f"{op} is hermitian:", is_hermitian(op))
#output    1*X0 is hermitian: True    1j*X0 is hermitian: False

### commutator​

commutator computes the commutator of two operators.

from quri_parts.core.operator import commutatorop1 = Operator({pauli_label("X0"): 1})op2 = Operator({pauli_label("Y0"): 1})print(f"[{op1}, {op2}]", "=", commutator(op1, op2))
#output    [1*X0, 1*Y0] = 2j*Z0

### truncate​

truncate removes PauliLabel from Operators if the corresponding coefficient is too small. (Default tolerance is 1e-8.)

from quri_parts.core.operator import truncateop = Operator(    {        pauli_label("Z0 Y1"): 1e-6,        pauli_label("X0 Y1"): 1e-10,        pauli_label("X0 Y2"): 1,    })print("original operator:\n", op)print("truncated operator:\n", truncate(op))print("truncated operator with tolerance = 1e-5:\n", truncate(op, 1e-5))
#output    original operator:     1e-06*Z0 Y1 + 1e-10*X0 Y1 + 1*X0 Y2    truncated operator:     1e-06*Z0 Y1 + 1*X0 Y2    truncated operator with tolerance = 1e-5:     1*X0 Y2

### is_ops_close​

is_ops_close checks if two operators are close up to some tolerance. For the Operators entered in the first and second arguments, this returns whether they are equal within an acceptable margin of error.

from quri_parts.core.operator import is_ops_closeop1 = Operator({pauli_label("X0 Y2"): 1})op2 = Operator(    {        pauli_label("Z0 Y1"): 1e-6,        pauli_label("X0 Y1"): 1e-10,        pauli_label("X0 Y2"): 1,    })print("op1", "=", op1)print("op2", "=", op2)print(f"op1 is close to op2 (atol=0):", is_ops_close(op1, op2))print(f"op1 is close to op2 (atol=1e-5):", is_ops_close(op1, op2, atol=1e-5))print(f"op1 is close to op2 (atol=1e-8):", is_ops_close(op1, op2, atol=1e-8))
#output    op1 = 1*X0 Y2    op2 = 1e-06*Z0 Y1 + 1e-10*X0 Y1 + 1*X0 Y2    op1 is close to op2 (atol=0): False    op1 is close to op2 (atol=1e-5): True    op1 is close to op2 (atol=1e-8): False

### get_sparse_matrix​

get_sparse_matrix converts Operators and PauliLabels to ascipy.sparse.spmatrix.

from quri_parts.core.operator import get_sparse_matrixop = Operator({    PAULI_IDENTITY: -8j,    pauli_label("X0 Y1"): 1})print(get_sparse_matrix(op))
#output      (0, 0)	-8j      (3, 0)	1j      (1, 1)	-8j      (2, 1)	1j      (1, 2)	-1j      (2, 2)	-8j      (0, 3)	-1j      (3, 3)	-8j

We can also get the explicit matrix representation

get_sparse_matrix(op).toarray()
#output    array([[0.-8.j, 0.+0.j, 0.+0.j, 0.-1.j],           [0.+0.j, 0.-8.j, 0.-1.j, 0.+0.j],           [0.+0.j, 0.+1.j, 0.-8.j, 0.+0.j],           [0.+1.j, 0.+0.j, 0.+0.j, 0.-8.j]])

It is often the case that the largest qubit containing a non-trivial Pauli matrix in an Operator or a PauliLabel is smaller than the number of qubits of the state it acts on. In this case, we can set the qubit count of the state the operator acts on with the n_qubit option.

get_sparse_matrix(op, n_qubits=3).toarray()
#output    array([[0.-8.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.-8.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.-8.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.-8.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.-8.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.-8.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.-8.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.-8.j]])