---
title: "Using ArcTN with Quimb"
description: "Optimize amplitude and expectation-value calculations in Quimb with ArcTN, integrate variational algorithms, and optionally limit slice sizes."
eyebrow: "Tutorials"
---

## Connecting ArcTN {#responsibilities}

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 {#install}

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](/docs/installation#python) for ArcTN setup and environment requirements.

## Computing a quantum-circuit amplitude {#circuit}

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, $\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 {#simplification}

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](https://quimb.readthedocs.io/en/latest/circuit/circuit.html) for its simplification rules.

## Adding a slice-size limit {#slicing}

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](/docs/slicing-modes).

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 {#variational}

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

$$
\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 $P_j$ is a product of Pauli operators and $c_j$ its coefficient. Quimb constructs an expectation network from the circuit and operator, containing the state $|\psi\rangle$, operator $P_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`](/docs/python-api#optimizer) 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](#slicing). 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](/docs/tutorial-reuse#variational-reuse). Different observables may produce different networks and cannot automatically share a path.

### Parameter gradients and other frontends {#gradients-and-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](https://quimb.readthedocs.io/en/latest/tensor/tensor-optimization.html).

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 {#manual-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](/docs/tensor-network#validation) for input requirements.

## Executing the contraction with ArcTN {#explicit-execution}

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](/docs/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 {#comparison}

- [Python API](/docs/python-api#optimizer): `ArcTNOptimizer` parameters and return values.
- [Third-party integrations](/docs/integrations): differences between path and contraction-tree interfaces.
- [Quimb Tensor Contraction](https://quimb.readthedocs.io/en/latest/tensor/tensor-contraction.html): Quimb tensor-network contraction interfaces.
- [Quimb expectation calculations](https://quimb.readthedocs.io/en/latest/circuit/circuit.html#compute-a-local-expectation): operators and outputs of `Circuit.local_expectation`.
- [Timing and CPU usage](/docs/performance-boundaries): distinguish path-optimization time from complete computation time.
