FermionicQDrift

class FermionicQDrift(num_terms, *, weights=None, rng=None)

Bases: object

A synthesis method that Trotterizes an Evolution via the qDRIFT protocol.

This method replaces the exact evolution \(e^{-i t H}\) of an Evolution gate by a randomized product formula: it draws num_terms samples from the Hamiltonian’s terms (or groups, if assigned), with each term sampled with a probability proportional to the magnitude of its sampling weight \(w_j\), and emits one Evolution gate per sample. Every sampled gate evolves its (unit-magnitude, sign-preserving) term (or, when the Hamiltonian carries groups, its whole sampled group) for the same time

\[\delta = \frac{\lambda t}{\texttt{num\_terms}}, \qquad p_j = \frac{w_j}{\lambda}, \qquad \lambda = \sum_j w_j,\]

so that each draw contributes \(\delta \cdot p_j = w_j t / \texttt{num\_terms}\). By default \(w_j = |c_j|\), the magnitude of the sampled term’s (or group’s) coefficient, which recovers the textbook qDRIFT normalization \(\lambda = \sum_j |c_j|\); the sign of \(c_j\) is not part of the weight but of the sampled operator, and is read off it directly. Supply weights to precompute that array once instead of deriving it on every call.

The ordered product of the sampled evolutions does not reproduce \(e^{-i t H}\) exactly; rather, its expectation over the sampling approximates the exact evolution, with an error that decreases as num_terms grows. Because the output depends on the random draws, it differs from run to run unless a fixed rng is supplied.

The method is registered as Evolution.QDrift, so it is on an equal footing with the product formulas FermionicLieTrotter and FermionicSuzukiTrotter:

F2FSynthesis({"Evolution": ("QDrift", (100,), {"rng": 42})})

A gate can also carry the method itself, as its synthesis, and be broken down by decompose() (or Qiskit’s Decompose pass). This is how each gate of a circuit gets its own num_terms, weights and rng:

Evolution(num_modes, operator, time, synthesis=FermionicQDrift(100, weights=weights))

Note

The sampled gates are marked Evolution.atomic: the random draw is the Trotterization this method performs, so the emitted gates are terminal factors and decompose() leaves them in place rather than splitting them further.

Note

A fixed rng reproduces the sequence of randomizations, not an individual member of it. Successive calls draw from the same generator, so they yield different circuits; replaying from the same seed reproduces all of them in the same order, but the nth circuit cannot be obtained without drawing the preceding ones first. Generating a batch of randomizations and recording the seed therefore works as expected (see Generate SqDRIFT circuits); addressing one member directly is not supported.

Hint

Terms that are diagonal in the occupation-number basis (that is, products of number operators) have no effect on the sampled bitstrings, so including them only increases the sampling overhead. Filter them out with filter_diagonal_terms() on the Hamiltonian before constructing the Evolution gate, rather than on every call: this method runs once per transpiled circuit, so filtering upstream avoids repeating the same filtering work for every circuit generated from the same Hamiltonian.

Caution

The scale of weights is not free: it sets the evolution time. Since \(\delta \cdot p_j = w_j t / \texttt{num\_terms}\) above depends on \(w_j\) itself and not merely on its share of \(\lambda\), rescaling every weight by \(\gamma\) leaves the distribution untouched but evolves for \(\gamma t\) rather than \(t\). Weights are therefore absolute magnitudes, not relative preferences, and only \(w_j = |c_j|\) reproduces \(e^{-i t H}\).

A distribution whose shape differs from \(|c_j|\) no longer approximates \(e^{-i t H}\) on its own either. Recovering the target evolution then requires reweighting the measured outcomes, which is the caller’s responsibility: this method emits circuits and cannot post-process their results. See weights.

See also

The qDRIFT protocol was introduced in arXiv:1811.08017. To skip the draws that cannot move a particle, see QDriftTrotterization.

Initializing an instance of this synthesis method can be done with the arguments below.

Parameters:
  • num_terms (int) – the number of terms to sample for the qDRIFT Trotterization. This equals the number of Evolution gates emitted per input gate; a larger value reduces the Trotterization error at the cost of a deeper circuit.

  • weights (Sequence[float] | np.ndarray | None) – the sampling weights to use instead of the coefficient magnitudes derived from the Hamiltonian. If None (the default), they are computed from the evolved operator on every call, which reproduces the textbook qDRIFT distribution. See weights for the expected length, the sign convention and the effect on the evolution time.

  • rng (np.random.Generator | int | None) – the random number generator (rng) to be used. When this is an int, the internal rng will be initialized with np.random.default_rng(seed=rng).

Raises:

ValueError – if weights is not one-dimensional, is empty, holds a non-finite or negative entry, or sums to zero.

Attributes

num_terms

The number of terms to include in the qDRIFT Trotterization.

weights: ndarray | None

The sampling weights \(w_j\), or None to derive them from the evolved operator.

Supplying them hoists their computation out of the synthesis: the default path recomputes them on every call, which is repeated work when generating an ensemble from a single Hamiltonian. Deriving them once with group_coeff_means() and passing the result here keeps this method stateless while paying that cost a single time. That is the reason to reach for this argument; passing anything other than the Hamiltonian’s own coefficient magnitudes changes which evolution the ensemble approximates (see the caution in the class docstring).

One entry is expected per group when the evolved operator carries groups, and per term otherwise; a mismatch raises ValueError. Because such an array describes one specific operator, it belongs to the gate of that operator: pass this method as the synthesis of that Evolution gate, so that every gate of a circuit can hold the weights of its own operator. This method is stateless and checks only the length against each gate it is given, so it cannot tell that the array was meant for another operator of the same size. That is why giving one method by gate name to F2FSynthesis suits a circuit with a single Evolution gate, or one that leaves the weights unset so that each gate derives its own.

The granularity follows what is sampled, which is why it is the grouping that decides it: a grouped operator is sampled group-wise, so a weight describes a whole group. Whether the grouping is the appropriate unit for a given operator is a property of that operator, not of this argument – see Group operator terms: use the operator structure, and groups_are_hermitian() to check the most common convention.

Entries must be non-negative. A weight is the magnitude \(h_j\) of the qDRIFT decomposition \(H = \sum_j h_j H_j\), in which a coefficient’s sign belongs to \(H_j\) rather than to \(h_j\) and is read off the operator directly, so a sign here would have nothing to describe.

Methods

sampler(gate)

Prepares the draws for a gate without taking any of them.

Parameters:

gate (Evolution) – the gate to sample.

Returns:

The sampler, from which gates can be drawn in a batch or one at a time.

Raises:

ValueError – if weights was supplied and its length does not match the number of groups (or terms) of the operator of gate.

Return type:

QDriftSampler

synthesize(gate)

Synthesizes the gate into num_terms sampled gates.

Parameters:

gate (Evolution) – the gate to synthesize.

Returns:

A FermionicCircuit holding one atomic Evolution gate per draw.

Raises:

ValueError – if weights was supplied and its length does not match the number of groups (or terms) of the operator of gate.

Return type:

FermionicCircuit