Skip to main content

Operators

Operators are a central tool for expressing and solving quantum many-body physics problems in Aleph. They underpin Aleph's ability to mix symbolic computation with numerical methods. Operators simplify the expression of quantum objects while avoiding costly memory allocations. This guide reviews the general operator framework.

Operator interface

All operators in Aleph implement the general Operator interface. This interface provides a consistent way to create, manipulate, and apply operators to quantum states. The key features of the Operator interface include:

  • Application to states: Operators can be applied to quantum states using the following syntax:
    • * for out-of-place operations (returns a new state): var new_state = op * state
    • >> for in-place operations (modifies the state): op >> state
  • State operation dispatch: Other forms of operation dispatch involving quantum states are handled by the operator. For example, expval(op, state) efficiently calculates the expectation value ψO^ψ\langle \psi | \hat{O} | \psi \rangle.
  • Matrix representation: Convert any operator to its dense or sparse matrix representation using matrix(op).
  • Composition: Combine operators using products and sums to build more complex expressions. Factory functions operator_prod() and operator_sum() facilitate this, though typically one uses the arithmetic operators * and + directly.
  • Introspection: Access operator properties programmatically, such as support() (acting sites) and norm() (spectral norm).

Concrete versus composite operators

Operators in Aleph can be broadly classified into two categories: concrete operators and composite operators.

  • Concrete operators are the basic building blocks that directly represent physical operations on quantum states. These include operators like Pauli matrices (X, Y, Z), identity (ID), and more complex ones like CNOT and SWAP. Concrete operators can be parameterized (e.g., rotation angles) or purely symbolic (e.g., Pauli matrices). For a list of specific concrete operators, see Spin-½ Operators.
  • Composite operators are constructed by combining concrete operators; they can be products and sums, but are not limited to these forms (e.g. see operator_function). They allow users to build complex Hamiltonians and quantum circuits from simpler components. Composite operators leverage the Operator interface to ensure that they can be manipulated and applied just like concrete operators.

Combining operators into sums and products

Quantum Hamiltonians and circuits are built by combining simple operators into larger expressions. In Aleph, this is done naturally using standard arithmetic.

Suppose we want to build the Hamiltonian for a transverse field Ising model on 4 qubits:

var h = 0.5;
var H = Z(0) * Z(1) + Z(1) * Z(2) + Z(2) * Z(3) // Interaction terms
+ h * (X(0) + X(1) + X(2) + X(3)); // Transverse field terms

Let's break down what's happening here:

  • Products: The * operator creates products of operators. For example, Z(0) * Z(1) represents the interaction between qubits 0 and 1. The resulting expression is an OperatorProduct.
  • Sums: The + operator sums different operator terms together. The entire Hamiltonian H is a sum of interaction terms and transverse field terms. The resulting expression is an OperatorSum.
  • Coefficients: Scalar coefficients (like 0.5 and h) can be multiplied with operators to scale them appropriately. The resulting expression is also an OperatorSum. This is because an OperatorSum is a linear combination of operators with coefficients.

In most cases, users need not worry about the underlying classes (OperatorProduct and OperatorSum) as Aleph handles these details automatically. However, for advanced use cases, users can directly create and manipulate these composite operators using the factory functions operator_prod() and operator_sum(). These functions provide more control over the construction of operator expressions and allow a more programmatic approach to building complex operators.

Building operator products with operator_prod()

For instance, suppose you would like to create a quantum circuit consisting of a series of gates applied in sequence. Working with 4 qubits, you could directly use * to build the product:

var circuit = Hgate(0) * Hgate(1) * Hgate(2) * Hgate(3) 
* CNOT(0,1) * CNOT(1,2) * CNOT(2,3);

While this works, it is clear that for larger circuits this becomes unwieldy. Instead, you can use operator_prod() to build the product step by step:

var L = 4; // Number of qubits
var circuit = operator_prod(); // Start with an empty product
for(m : [0..L])
{
circuit *= Hgate(m); // Apply Hadamard to each qubit
}
for(m : [0..L-1])
{
circuit *= CNOT(m, m+1); // Apply CNOTs in sequence
}

We can go further and wrap the above into functions for reusability:

fun hadamards(L) {
return foldl(map(reversed([0..L]), Hgate), `*`, operator_prod())
}
fun cnot_chain(L) {
var prod = operator_prod();
for(m : [0..L-1])
{
prod *= CNOT(m, m+1);
}
return prod;
}
var L = 4;
var circuit = hadamards(L) * cnot_chain(L);
info

foldl and map are functional programming constructs in Aleph that allow for concise and expressive transformations and reductions over collections.

It is also possible to initialize operator_prod() directly with an operator expression, rather than starting empty:

// Create from an expression
var prod = operator_prod(X(0) + X(1)); // -> X(0) + X(1)
prod *= Y(5); // -> (X(0) * X(1)) * Y(5)

Note that an OperatorSum can be nested within an OperatorProduct, and vice versa.

info

While operator_prod() cannot store scalar coefficients directly, it is possible to scale a product by putting the product in an OperatorSum or putting a single-term OperatorSum (e.g. 2.0 * X(0)) in an OperatorProduct.

You can access individual operators within a product using the [] accessor. Indices read from left to right (index 0 is the leftmost operator):

var prod = Z(0) * RotZ(pi/3, 1) * X(2);
print(prod[0]); // -> Z(0)
print(prod[1]); // -> RotZ(pi/3, 1)
print(prod[2]); // -> X(2)

Building operator sums with operator_sum()

Quantum circuits naturally involve products of operators, but measurements and Hamiltonians often involve sums of operators. Aleph provides the operator_sum() factory function to facilitate building such sums.

Suppose we want to build the Hamiltonian for a transverse field Ising model on an arbitrary number of qubits. The arithmetic approach described above becomes cumbersome. Instead, we can use operator_sum() to build the Hamiltonian step by step:

var L = 6; // Number of qubits
var h = 0.5; // Transverse field strength
var H = operator_sum(as_real); // Start with an empty sum with real coefficients
for(m : [0..L-1])
{
H += Z(m) * Z(m + 1); // Interaction term
}
for(m : [0..L])
{
H += h * X(m); // Transverse field term
}

Again, we can wrap this into a function for reusability:

def tfim_hamiltonian(L, h) {
var H = operator_sum(as_real);
H += foldl(map([0..L-1], fun(m) { return ZZ(m, m + 1); }), `+`, operator_sum(as_real));
H += h * foldl(map([0..L], X), `+`, operator_sum(as_real));
return H;
}
var L = 6;
var h = 0.5;
var H = tfim_hamiltonian(L, h);

You may have noticed the use of as_real in the above examples. as_real is a type tag (like as_complex) to specify the type of coefficients to be used in the operator sum. If you do not specify a type tag, operator_sum() defaults to using complex coefficients, which are more general but also more memory intensive. If you know your coefficients will always be real, using as_real can lead to better performance and lower memory usage.

Choosing coefficient types

Use as_real when all coefficients are known to be real to improve performance and memory efficiency. Use as_complex (or omit the type specifier) to use complex coefficients.

It is also possible to initialize operator_sum() directly with an expression, specifying the coefficient type if needed:

// Initialize with real coefficients
var H_real = operator_sum(2.0*X(0) + 3.0*Y(1), as_real);

// Initialize with complex coefficients
var H_complex = operator_sum((1.0+1.0i)*X(0) + 2.0i*Y(1), as_complex);
Coefficient management

Operator sums do not automatically merge like terms. For example, X(0) + X(0) stores two separate terms with coefficient 1.0 each. Use the merged() function (see Operator Transformations) to combine like terms when needed.

You can access individual terms in an operator_sum using the [] accessor. Each element is a pair where .first is the coefficient and .second is the operator.

var sum = operator_sum(2.0 * X(0) + 3.5 * Y(1) + 1.0 * Z(2), as_real);

// Access operators
print(sum[0].second); // -> X(0)
print(sum[1].second); // -> Y(1)
print(sum[2].second); // -> Z(2)

// Access coefficients
print(sum[0].first); // -> 2.0
print(sum[1].first); // -> 3.5
print(sum[2].first); // -> 1.0

You can also extract all coefficients at once using get_coefficients():

// Real operator sum
var H_real = operator_sum(as_real);
H_real += 2.0 * X(0) + 3.5 * Y(1);
var real_coeffs = get_coefficients(H_real); // Returns [2, 3.5]

// Complex operator sum
var H_complex = operator_sum(as_complex);
H_complex += (1.0 + 2.0i) * X(0) + (3.0 - 1.5i) * Y(1);
var complex_coeffs = get_coefficients(H_complex); // Returns [1 + 2i, 3 - 1.5i]

This is useful for analyzing the structure of your Hamiltonian or inspecting individual terms before applying transformations.

Interacting with vectors

Operators can be applied to compatible vectors. The * operator performs out-of-place application, returning the result in a new vector. The >> operator performs in-place application, modifying the vector in-place. For instance:

var vec = [0.0, 1.0, 2.0, 3.0]; // Represents a state vector
var xv = X(0) * vec // Out-of-place application, vec remains unchanged
// xv = [1.0, 0.0, 3.0, 2.0]
X(1) >> vec // In-place application, vec is modified
// vec = [1.0, 3.0, 0.0, 2.0]

One might ask: why not always use the in-place operator >>? For local operators, like X used in the example above, using >> is more memory efficient since it avoids allocating a new vector. This is because local operators allow for efficient in-place modifications where elements interact in groups that can be copied to a small auxiliary buffer. However, there are scenarios where the out-of-place operator * is more efficient. For example, when applying a non-local operator (e.g., a long-range interaction term) to a vector, the in-place operation may require so many auxiliary buffers that it leads to increased memory usage and slower performance. In such cases, using the out-of-place operator * is preferred as it avoids the overhead of managing multiple auxiliary buffers.

As a general rule of thumb:

  • Use >> when "evolving" states with local operators or chains of local operators (e.g., quantum circuits represented as OperatorProducts).
  • Use * when accumulating the products of several operators (e.g., summing the effects of multiple terms in a Hamiltonian or OperatorSum).
tip

Use >> with operator_prod and * with operator_sum to optimize memory usage and performance when applying operators to vectors.

Operator introspection

Aleph provides several functions for programmatic operator introspection. This allows users to query properties of operators, such as their support (the sites they act on) and norms.

Getting operator support

Use support() to get the set of sites that an operator acts on:

var op1 = X(0);
var sup1 = support(op1); // Returns [0]

var op2 = X(0) * Y(2) * Z(5);
var sup2 = support(op2); // Returns [5, 2, 0]

var op3 = CNOT(3, 7);
var sup3 = op3.support(); // Returns [7, 3]
Current limitation

This version supports operator_prod and single operators. Support for operator_sum may not work correctly in the current version.

Computing operator norm

By default, we use the spectral norm, i.e., the largest eigenvalue in absolute value of the operator. You can compute the norm of any operator using the norm() function:

var op1 = norm(X(0));           // Norm of a single Pauli operator
var op2 = norm(X(0) + Y(1)); // Norm of an `operator_sum`
var op3 = norm(2.0 * Z(0)); // Norm includes the coefficient

For a term with a coefficient and an operator, the norm behaves as you expect and combines the two:

coefficient×operator norm|\text{coefficient}| \times \text{operator norm}

This effective norm is used by the prune transformations to determine which terms are negligible in an operator_sum.

For operator_sum we instead employ an effective norm that uses the triangle inequality:

mAmmAm|| \sum_m A_m || \leq \sum_m ||A_m||

A number of known norms are encoded, and unitarity is exploited to ensure expensive calculations are avoided in these operations when possible.

Documentation Contributors

Jonathon Riddell

Eunji Yoo

Sebastien J Avakian

Vincent Michaud-Rioux