Using ArcTN with Quimb

Optimize amplitude and expectation-value calculations in Quimb with ArcTN, integrate variational algorithms, and optionally limit slice sizes.

On this page

Connecting ArcTN

Pass ArcTNOptimizer as the Quimb optimize argument to optimize contraction order with ArcTN. Quimb constructs the tensor network, ArcTN returns a ContractionTree with a path and optional slicing information, and Quimb/Cotengra performs the numerical computation.

Installation

After installing the ArcTN Python package containing Light / Heavy, install the Quimb integration dependencies in the same virtual environment:

bash
python -m pip install quimb cotengra opt_einsum

See Installation for ArcTN setup and environment requirements.

Computing a quantum-circuit amplitude

Build the circuit, then specify ArcTNOptimizer for the amplitude calculation:

python
import numpy as np
import quimb.tensor as qtn
from arctn import ArcTNOptimizer

n = 8
rng = np.random.default_rng(0)
circ = qtn.Circuit(n)
for depth in range(6):
    for q in range(n):
        circ.apply_gate("RY", float(rng.uniform(0, 2 * np.pi)), q)
    for q in range(depth % 2, n - 1, 2):
        circ.apply_gate("CNOT", q, q + 1)

optimizer = ArcTNOptimizer(preset="light", seed=0)
amplitude = circ.amplitude("0" * n, optimize=optimizer)
print(amplitude)

The output is the amplitude for an all-zero final bitstring when starting from the all-zero initial state, 000U000\langle 00\cdots0|U|00\cdots0\rangle. The probability of measuring this bitstring is its squared modulus. ArcTN optimizes the contraction order of the network used for this calculation.

Quimb preprocessing

Quimb may simplify the task network before passing it to ArcTN. Its defaults are usually sufficient. To disable optional simplification, set simplify_sequence="" in the circuit task call. This parameter controls simplification before Quimb passes on the network.

If simplification reduces the task to a simple contraction, Quimb/Cotengra may compute it directly without calling ArcTN. See the Quimb circuit documentation for its simplification rules.

Adding a slice-size limit

Using the circuit above, convert the amplitude task into a tensor network and set target_size:

python
tn = circ.amplitude_tn("0" * n)
optimizer = ArcTNOptimizer(
    preset="heavy", seed=0,
    target_size=2**28,
    slicing_mode="fixed",
)
value = tn.contract(
    all, output_inds=(), optimize=optimizer,
)

Quimb/Cotengra obtains a contraction tree containing the path and sliced indices through ArcTNOptimizer.search(), then computes and sums the slices. fixed preserves the searched path; dynamic permits local path changes. See Fixed and dynamic slicing.

The largest binary contraction result within each slice must not exceed target_size elements; this is not a total-memory cap. Slicing may be unnecessary if the original path already satisfies the limit.

Simple contractions with only one or two input tensors may bypass the external optimizer. If such networks must also enforce target_size, call arctn_tree or arctn_contract directly.

Expectation values for VQE and QAOA

A variational quantum eigensolver (VQE) prepares a state with a parameterized circuit U(θ)U(\theta) and computes the Hamiltonian expectation:

H=jcjPj,E(θ)=jcj0U(θ)PjU(θ)0.\begin{aligned} H&=\sum_j c_j P_j,\\ E(\theta)&=\sum_j c_j\langle 0|U(\theta)^\dagger P_j U(\theta)|0\rangle. \end{aligned}

Here PjP_j is a product of Pauli operators and cjc_j its coefficient. Quimb constructs an expectation network from the circuit and operator, containing the state ψ|\psi\rangle, operator PjP_j, and conjugate-transposed state ψ\langle\psi|, without first building the full state vector. Circuit.local_expectation accepts ArcTNOptimizer through its optimize argument.

The quantum approximate optimization algorithm (QAOA) also computes expectation values, but its objective depends on the combinatorial optimization problem. Both methods can delegate contraction-order search for each expectation calculation to ArcTN.

The pseudocode below illustrates the relationship between energy evaluation and parameter optimization without expanding library-specific calls.

pseudocode
Inputs: initial parameters theta_initial and Hamiltonian terms (c_j, P_j)
function energy(theta):
    circuit = build a parameterized circuit using theta
    value = 0
    For each Hamiltonian term (c_j, P_j):
        network = build an expectation-value tensor network from circuit and P_j
        tree = use ArcTN to optimize the network path, adding slicing if needed
        expectation = contract network along tree, summing slices if needed
        value = value + c_j * expectation
    return the real part of value
theta_star = minimize energy from theta_initial using a classical optimizer

With Quimb, call Circuit.local_expectation with its optimize argument set to ArcTNOptimizer to perform the network construction, path optimization, and numerical computation shown in the pseudocode. The application separately invokes the classical optimizer: ArcTN selects contraction order, not circuit parameters. Expectations here are computed directly, without finite-shot measurement sampling.

Application Circuit calculation returns Outer algorithm does
VQE Expectation of each Hamiltonian term Weighted summation and energy minimization
QAOA Expectation of the problem cost Hamiltonian Maximize or minimize the problem objective and update γ\gamma, β\beta
Quantum machine learning Selected observable expectations or output probabilities Compute model loss and update trainable parameters

To limit intermediates, add target_size=2**20, slicing_mode="fixed" to ArcTNOptimizer, with the same meaning as in the slicing example. Quimb/Cotengra executes the returned slices; this setting is not a process-wide memory cap.

An ArcTNOptimizer object does not cache paths. Reuse is controlled by the caller. For parameterized networks with fixed structure, explicitly save a path and execute it repeatedly; see Reusing paths as parameters change. Different observables may produce different networks and cannot automatically share a path.

Parameter gradients and other frontends

ArcTN path optimization does not compute circuit-parameter gradients. Gradient-free optimization can use the energy function above directly. When gradients are needed, eligible gates can use the parameter-shift rule: evaluate expectations at shifted parameters and combine them into gradients. Frontend-supported automatic differentiation is another option. Native Rust execution does not provide parameter gradients; with external array backends, verify that the operations actually used support differentiation. See Quimb optimization documentation.

Other circuit frontends must first convert the task to a tensor network. Callers using Quimb or opt_einsum contraction interfaces can reuse the existing integration. Framework-specific device or execution interfaces require an adapter. ArcTNOptimizer itself is neither a Qiskit Estimator or Sampler nor a PennyLane device.

Building a tensor network

Build a four-tensor network from arrays and indices:

python
import numpy as np
import quimb.tensor as qtn
from arctn import ArcTNOptimizer

rng = np.random.default_rng(0)
ta = qtn.Tensor(rng.normal(size=(2, 3)), inds=("a", "x"))
tb = qtn.Tensor(rng.normal(size=(3, 4)), inds=("x", "y"))
tc = qtn.Tensor(rng.normal(size=(4, 5)), inds=("y", "z"))
td = qtn.Tensor(rng.normal(size=(5, 2)), inds=("z", "b"))
tn = qtn.TensorNetwork([ta, tb, tc, td])

result = tn.contract(
    all, output_inds=("a", "b"),
    optimize=ArcTNOptimizer(preset="light", seed=0),
)
print(result.data.shape)  # (2, 2)

Matching index names indicate connections and must have consistent dimensions. output_inds=("a", "b") retains two output indices in the specified axis order; use output_inds=() for a scalar result. See Tensor networks and contraction paths for input requirements.

Executing the contraction with ArcTN

You can also extract structure and arrays from a Quimb network and pass them to arctn_contract for path optimization and numerical execution:

python
from arctn import arctn_contract

tensors = list(tn)
value = arctn_contract(
    [tuple(t.inds) for t in tensors],
    ("a", "b"),
    dict(tn.ind_sizes()),
    [t.data for t in tensors],
    preset="heavy", backend="numpy",
)
value = value * (10.0 ** float(tn.exponent))

backend="numpy" uses the NumPy array backend; native is also available. See Execution backends. If Quimb stores a separate scale factor in tn.exponent, multiply the extracted-array result by 10**tn.exponent, as in the final line above. Without separate scaling, this exponent is zero. Quimb handles this when using tn.contract, so do not multiply it in again.

Related documentation