Operators

ParityOS offers a wide and flexible range of quantum operators, such as

  • Elementary quantum operators (Pauli gates, Hadamard gate, phase gate, etc.)

  • Quantum controlled operators (CNOT gate, Toffoli gate, controlled rotations, etc.)

  • Classically controlled operators (operators conditioned on classical control bits)

  • Rotation operators

  • Measurement and reset operations

  • Operator composition (tensor products, linear combinations like Hamiltonian)

The following is a coarse overview of the hierarchy of classes describing operators:

OperatorLike (interface for all operator-like operations on qubits) 
├── Operator (single operators with operator arithmetic) 
│   ├── ConcreteOperator (atomic operations) 
│   │   ├── SingleQubitOperator (X, Y, Z, H, S, T, ...) 
│   │   ├── OrderedMultiQubitOperator (ECR, ...) 
│   │   └── UnorderedMultiQubitOperator (Swap, iSwap, ...) 
│   ├── RotationOperator (parametrized rotations) 
│   │   ├── GeneralRotationOperator (turn any hermitian OperatorPolynomial into a rotation operator)
│   │   └── Concrete implementations (RX, RY, RZ, RXX, RYY, RZZ, ...) 
│   ├── ControlledOperator (operators triggered by control bits) 
│   │   ├── GeneralControlledOperator (turn any operator into a controlled operator)
│   │   └── Concrete implementations (CX, CY, CZ, CRX, CRY, CRZ, CCX, ...) 
│   ├── ConditionalOperator (classical bit conditions)
│   └── Measurement (measurements: MX, MZ) 
├── Circuit (circuits as sequence of quantum operators) 
└── OperatorCompound (higher order objects composed of Operator instances)`
    ├── OperatorProduct (operator tensor products) 
    └── OperatorPolynomial (linear combinations of operator tensor products) 

Operators in ParityOS always require concrete target qubits as arguments upon instantiation. This means that operator instances represent concrete quantum operations that act non-trivially only on a certain subspace of an overall Hilbert space, namely that which is spanned by the state space of the explicit target qubits. Operators are assumed to act trivially (as the identity) on all other qubits, that are not part of their explicit qubits (like e.g. in a quantum Circuit).

Consequently, the top level OperatorLike interface describes anything operator-like that acts on qubits. This encompasses actual Operators, such as elementary atomic operators, rotation operators, quantum and classically controlled operators and operations such as measurements and resets, as well as Operator compounds, such as tensor products (OperatorProduct) and linear combinations thereof (OperatorPolynomial), and Circuits as sequences of Operators.

As classes are first-class objects in Python, the general concept of an operator as an abstract operation that is not tied to a specific set of target qubits is represented by class objects themselves (like e.g. representing the Pauli bases by the corresponding operators X, Y and Z).

Note

Operators in ParityOS are purely representative, they don’t carry any numerical information about their matrix representations or algebraic properties.

Operator Properties

Certain operator properties can be checked either by calling methods or checking if they are instances of some abstract base class.

Hermiticity

It is possible to check whether an operator is hermitian through their is_hermitian property or by checking whether it is an instance of HermitianOperator.

from parityos.bits import Qubit
from parityos.operators.elementary_operator import S, X
from parityos.operators.operator import HermitianOperator

qubit = Qubit(0)

x = X(qubit)
assert isinstance(x, HermitianOperator)
assert x.is_hermitian
assert x.get_hermitian_conjugate() == x

s = S(qubit)
assert not isinstance(s, HermitianOperator)
assert not s.is_hermitian
assert s.get_hermitian_conjugate() != s

Note

Operators (or compounds thereof) can be hermitian whithout being a subclass of HermitianOperator, like e.g. an OperatorProduct with hermitian factors, or an OperatorPolynomial with hermitian terms and real coefficients.

Parameterized

Some operators can be parameterized by symbolic parameters (e.g. rotation angles of RotationOperator). To check whether an operator has symbolic parameters, check whether it is an instance of Parameterized and if its parameters property is empty.

Note

Operators can be subclasses of Parameterized, even if they do not have any symbolic parameters, e.g. when a numeric value is used instead of a symbolic parameter, as in RX(qubit, 2.2).

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X
from parityos.operators.rotation_operator import RX
from parityos.utils.parameter import Parameter, Parameterized

qubit = Qubit(0)

x = X(qubit)
assert not isinstance(x, Parameterized)

rx_numeric = RX(qubit, 2.2)
assert isinstance(rx_numeric, Parameterized) and not rx_numeric.parameters

rx_symbolic = RX(qubit, Parameter("theta"))
assert isinstance(rx_numeric, Parameterized) and len(rx_symbolic.parameters) == 1

Single-Qubit vs. Multi-Qubit Operators

Single-qubit operators are subclasses of SingleQubitOperator and have an additional qubit field, returning a single qubit. They still implement the qubits and ordered_qubits properties from the OperatorLike interface, returning however single element containers.

Multi-qubit operators naturally don’t have a qubit field and just implement the regular interface fields. They are subclasses of either OrderedMultiQubitOperator or UnorderedMultiQubitOperator.

For OrderedMultiQubitOperators, the order of the qubit matters, so they are stored in an ordered tuple. UnorderedMultiQubitOperators represent operators, which are symmetric under the permutation of their qubits, so the order of the qubits doesn’t matter and they are stored as an unordered frozenset. Both kinds of multi-qubit operators implement the full OperatorLike interface, where OrderedMultiQubitOperator.qubits just returns a frozenset of the ordered qubits, and UnorderedMultiQubitOperator.ordered_qubits returns a sorted tuple of the (unordered) qubits (e.g. for unambiguous display or serialization).

Single-qubit and multi-qubit operators always come with a pre-defined number of qubits and are subclasses of ConcreteOperator (see the Elementary Operators section below).

from parityos.bits import Qubit
from parityos.operators.elementary_operator import ECR, Swap, X
from parityos.operators.operator import (
    OrderedMultiQubitOperator,
    SingleQubitOperator,
    UnorderedMultiQubitOperator,
)

qubit1 = Qubit(0)
qubit2 = Qubit(1)

# single-qubit operator
assert X.n_qubits == 1

x = X(qubit1)
assert isinstance(x, SingleQubitOperator)
assert x.ordered_qubits[0] == qubit1

# multi-qubit operator
assert ECR.n_qubits == 2

ecr = ECR((qubit1, qubit2))
assert isinstance(ecr, OrderedMultiQubitOperator)

# ordered operators are not invariant under qubit permutation
assert ECR((qubit1, qubit2)) != ECR((qubit2, qubit1))

# (symmetric) multi-qubit operator
assert Swap.n_qubits == 2

s = Swap((qubit1, qubit2))
assert isinstance(s, UnorderedMultiQubitOperator)

# symmetric operators are invariant under qubit permutation
assert Swap((qubit1, qubit2)) == Swap((qubit2, qubit1))

Elementary Operators

Elementary operators are concrete fundamental operators. They are subclasses of ConcreteOperator and have a fixed pre-defined number of qubits through a pre-defined class variable n_qubits.

Examples are well known fundamental operators, such as the Pauli operators X, Y, Z, the Clifford gates H or CX or the non-Clifford gates S and T, as well as the rotations RX, RY, RZ and controlled rotations CRX, CRY, CRXZ.

All currently supported elementary operators that are not rotations or controlled operators can be imported from the parityos.operators.elementary_operator module.

Rotation Operators

Rotation operators are defined as exponentials of hermitian operators with a complex angle.

\[R(A) = \exp\left(- i \frac{\varphi}{2}O\right)\]

where \(O\) is any hermitian ConcreteOperator, OperatorProduct or OperatorPolynomial and \(\varphi\) is the real angle of the rotation in radians.

All rotations are subclasses of the RotationOperator base class, which defines an exponent field corresponding to \(O\) above. The GeneralRotationOperator class allows for turning any hermitian operator into a rotation operator and directly takes \(A\) as an exponent argument. As a rotation angle is not clearly defined for an OperatorPolynomial as exponent, the angle is assumed to be 1 for any GeneralRotationOperator.

In addition there are concrete implementations of common rotation operators with single term exponent, which are parameterized by a single angle \(\varphi\). All such operators are subclasses of the SingleAngleRotation base class, which defines a real valued angle field (which can be numeric or symbolic) and the exponent field then returns \(O\).

All concrete rotation operators are also elementary operators and either single-qubit or multi-qubit operators with a pre-defined number of qubits. As such they are also subclasses of ConcreteOperator.

Note

If a GeneralRotationOperator gets instantiated with an exponent that would make it correspond to one of the concrete implemented rotations, an instance of that concrete class is created instead.

All currently supported rotation operators (general and concrete) can be imported from the parityos.operators.rotation_operator module.

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X, Z
from parityos.operators.operator import (
    ConcreteOperator,
    OrderedMultiQubitOperator,
    SingleQubitOperator,
    UnorderedMultiQubitOperator,
)
from parityos.operators.rotation_operator import RX, RXZ, RZZ, GeneralRotationOperator

qubit1 = Qubit(0)
qubit2 = Qubit(1)

# single-qubit rotation
assert RX.n_qubits == 1

rx = RX(qubit1, 2)
assert isinstance(rx, ConcreteOperator)
assert isinstance(rx, SingleQubitOperator)
assert rx.angle == 2

# multi-qubit rotation
assert RXZ.n_qubits == 2

rxz = RXZ((qubit1, qubit2), -1)
assert isinstance(rxz, ConcreteOperator)
assert isinstance(rxz, OrderedMultiQubitOperator)
assert RXZ((qubit1, qubit2), -1) != RXZ((qubit2, qubit1), -1)

# (symmetric) multi-qubit rotation
assert RZZ.n_qubits == 2

rzz = RZZ((qubit1, qubit2), -1)
assert isinstance(rzz, ConcreteOperator)
assert isinstance(rzz, UnorderedMultiQubitOperator)
assert RZZ((qubit1, qubit2), -1) == RZZ((qubit2, qubit1), -1)

# general rotations correspond to concrete implementations
assert GeneralRotationOperator(2 * X(qubit1)) == rx
assert GeneralRotationOperator(-Z(qubit1) * Z(qubit2)) == rzz

Quantum Controlled Operators

Quantum controlled operators are multi-qubit operators where a target operator is applied to on or more “target” qubits depending on the quantum state of one or more “control” qubits. They are subclasses of the ControlledOperator base class, which defines the following abstract properties:

Controlled operators with single control or target qubits additionally have a single qubit control_qubit or target_qubit field (see also section Single-Qubit vs. Multi-Qubit Operators).

Controlled operators are always symmetric under the permutation of their control qubits, but not necessarily in their target qubits.

The GeneralControlledOperator class directly takes a target_operator and an arbitrary amount of control_qubits as argument, which allows for turning any (non-controlled) Operator into an arbitrarily controlled operator.

If the target operator is a tensor product, the resulting controlled operator is equivalent to a (commuting) sequence of controlled versions of the target product’s factors (e.g. \(\mathrm{CXX} = \mathrm{CX} \otimes \mathrm{CX}\)). Such operators can be decomposed into their controlled factors with the decompose method.

In addition there are concrete implementations of common controlled operators with pre-defined control and target qubit numbers and target operators. As such they are also subclasses of ConcreteOperator.

Note

If a GeneralControlledOperator gets instantiated with a target_operator and control_qubits that make it equivalent to one of the concrete implemented controlled operators, an instance of that concrete class is created instead.

All currently supported quantum controlled operators (general and concrete) can be imported from the parityos.operators.controlled_operator module.

from parityos.bits import Qubit
from parityos.operators.controlled_operator import CCX, CRXZ, CX, GeneralControlledOperator
from parityos.operators.elementary_operator import X
from parityos.operators.operator import ConcreteOperator
from parityos.operators.rotation_operator import RXZ

control_qubit1 = Qubit("c1")
control_qubit2 = Qubit("c2")
target_qubit1 = Qubit(0)
target_qubit2 = Qubit(1)

# single control, single target
assert CX.n_control == 1
assert CX.n_target == 1

cx = CX(control_qubit1, target_qubit1)
assert isinstance(cx, ConcreteOperator)
assert cx.target_operator == X(target_qubit1)

# multi control, single target
assert CCX.n_control == 2
assert CCX.n_target == 1

ccx = CCX((control_qubit1, control_qubit2), target_qubit1)
assert isinstance(ccx, ConcreteOperator)
assert ccx.target_operator == X(target_qubit1)

# single control, multi target
assert CRXZ.n_control == 1
assert CRXZ.n_target == 2

crx = CRXZ(control_qubit1, (target_qubit1, target_qubit2), -2)
assert isinstance(crx, ConcreteOperator)
assert crx.target_operator == RXZ((target_qubit1, target_qubit2), -2)
assert CRXZ(control_qubit1, (target_qubit1, target_qubit2), -2) != CRXZ(
    control_qubit1, (target_qubit2, target_qubit1), -2
)

# general controlled operators correspond to concrete implementations
assert GeneralControlledOperator(control_qubit1, X(target_qubit1)) == cx
assert GeneralControlledOperator((control_qubit1, control_qubit2), X(target_qubit1)) == ccx

# controlled tensor product
assert set(
    GeneralControlledOperator(control_qubit1, X(target_qubit1) * X(target_qubit2)).decompose()
) == {CX(control_qubit1, target_qubit1), CX(control_qubit1, target_qubit2)}

Classically Controlled Operators

The ConditionalOperator class allows for controlling quantum operators with classical control flows through classical control bits (Cbits). It takes a boolean condition on an arbitrary number of classical bits in the form of a CbitExpression and a target operator that gets triggered if the boolean condition evaluates to True.

Classically controlled operators can be imported from the parityos.operators.conditional_operator module.

from parityos.bits import Cbit, Qubit
from parityos.operators.conditional_operator import ConditionalOperator
from parityos.operators.elementary_operator import X, Y, Z

cbit1 = Cbit(0)
cbit2 = Cbit(1)
cbit3 = Cbit(2)
qubit = Qubit(0)

# A Pauli X operator that gets triggered if cbit1 is `True`
cx = ConditionalOperator(cbit1, X(qubit))

# A Pauli Z operator that gets triggered if all classical bits are `True`
cz = ConditionalOperator(cbit1 & cbit2 & cbit3, Z(qubit))

# A Pauli Y operator that gets triggered for the following classical states: 000, 001, 010, 011, 101, 111
cz = ConditionalOperator(~cbit1 | (cbit1 ^ cbit2), Y(qubit))

Measurements and Reset

Measurements are single-qubit operators and are subclasses of the Measurement base class. In addition to the single qubit to measure, they also take a single classical bit (Cbit) on which the measurement result is stored. Only measurements in the Pauli Z (MZ) and X (MX) basis are supported.

The Reset operation is a single-qubit operator that resets the target qubit to the state \(|0\rangle\).

All currently supported measurement and reset operations can be imported from the parityos.operators.measurement module.

Operator Products, Polynomials and Operator Arithmetic

An OperatorProduct represents a tensor product of individual Operators that do not share any qubits. An OperatorPolynomial represents a linear combination of OperatorProducts with numeric or symbolic coefficients.

For example

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X, Z
from parityos.operators.operator_product import OperatorProduct

product = OperatorProduct([X(Qubit(0)), X(Qubit(1)), Z(Qubit(2))])

corresponds to \(X_0 \otimes X_1 \otimes Z_2\), while

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X, Y, Z
from parityos.operators.operator_polynomial import OperatorPolynomial
from parityos.operators.operator_product import OperatorProduct

q0, q1 = Qubit(0), Qubit(1)

polynomial = OperatorPolynomial(
    {
        OperatorProduct([X(q0), X(q1)]): 2,
        OperatorProduct([Y(q0), Y(q1)]): 2,
        OperatorProduct([Z(q0), Z(q1)]): -1,
    }
)

corresponds to \(2(X_0 \otimes X_1 + Y_0 \otimes Y_1) - Z_0 \otimes Z_1\). Here subscript indices denote target qubit indices.

Operator product and polynomial assembly can be greatly simplified by using operator arithmetic. By using the arithmetic operators + (addition), - (subtraction/negation) and * (tensor product), the above can be simplified to

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X, Y, Z

q0, q1, q2 = Qubit(0), Qubit(1), Qubit(2)

product = X(q0) * X(q1) * Z(q2)
polynomial = 2 * (X(q0) * X(q1) + Y(q0) * Y(q1)) - Z(q0) * Z(q1)

As operators acting on disjoint qubit sets commute, operator products and polynomials are symmetric under the permutation of their constituent operators.

from parityos.bits import Qubit
from parityos.operators.elementary_operator import X, Z

q0, q1, q2 = Qubit(0), Qubit(1), Qubit(2)

assert X(q0) * X(q1) * Z(q2) == X(q1) * X(q0) * Z(q2) == X(q0) * Z(q2) * X(q1)

Identity Operator

The identity operator is represented as an empty OperatorProduct. This is consistent with the inherent assumption that all operators (elementary or compound) only act non-trivially on their explicitly given qubits, and trivially as the identity on all other possibly present qubits (e.g. in a Circuit).

ParityOS defines parityos.operators.operator_product.I as a singleton instance of the identity operator.