BosonicQDrift

class BosonicQDrift(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 BosonicLieTrotter and BosonicSuzukiTrotter:

B2BSynthesis({"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=BosonicQDrift(100, weights=weights))

Note

The sampled gates are marked BosonicGate.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. Each of them carries over the BosonicGate.synthesis method of the gate it was sampled from, even though an atomic gate never consults it.

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. Addressing one member directly is not supported.

Hint

Terms that are diagonal in the occupation-number basis (i.e. 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}\).

Note

Every sampled term is emitted; none are pruned as trivial. A bosonic mode’s occupation is an unbounded non-negative integer, so a creation operator never annihilates a state, and knowing that a mode is occupied says nothing about which terms act trivially on it. The only bosonic term that is guaranteed to vanish is one annihilating a mode known to hold zero bosons (\(b \lvert 0 \rangle = 0\)), which is too narrow a case to filter for here.

See also

The qDRIFT protocol was introduced in arXiv:1811.08017.

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 ordering the groups once with group_order(), which hoists the group lookup out of the same loop) 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 B2BSynthesis 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. Sampling from a signed (quasi-probability) distribution is a separate feature: it additionally requires the accumulated sign of the sampled entries to be reported back for post-processing, which this method does not do.

Caution

These are absolute magnitudes, not relative preferences: their sum also sets the evolution time, so rescaling every entry by \(\gamma\) evolves for \(\gamma t\) while sampling identically. See the class docstring.

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 BosonicCircuit 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:

BosonicCircuit