# FermionicQDrift

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

A synthesis method that Trotterizes an [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution) via the qDRIFT protocol.

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

This method replaces the exact evolution $e^{-i t H}$ of an [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution) gate by a
randomized product formula: it draws `num_terms` samples from the Hamiltonian’s terms (or
[`groups`](qiskit_fermions.operators.FermionOperator.html.md#qiskit_fermions.operators.FermionOperator.groups), if assigned), with each term sampled
with a probability proportional to the magnitude of its sampling weight $w_j$, and emits
one [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.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`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.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`](qiskit_fermions.circuit.library.synthesis.FermionicLieTrotter.html.md#qiskit_fermions.circuit.library.synthesis.FermionicLieTrotter) and [`FermionicSuzukiTrotter`](qiskit_fermions.circuit.library.synthesis.FermionicSuzukiTrotter.html.md#qiskit_fermions.circuit.library.synthesis.FermionicSuzukiTrotter):

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

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

```python
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()`](qiskit_fermions.circuit.FermionicCircuit.html.md#qiskit_fermions.circuit.FermionicCircuit.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](../guides/sqdrift.html.md#sqdrift-getting-started));
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()`](qiskit_fermions.operators.terms.filter_diagonal_terms.html.md#qiskit_fermions.operators.terms.filter_diagonal_terms) on the Hamiltonian
*before* constructing the [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.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`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.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`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.weights).

#### SEE ALSO
The qDRIFT protocol was introduced in [arXiv:1811.08017](https://arxiv.org/abs/1811.08017).
To skip the draws that cannot move a particle, see [`QDriftTrotterization`](qiskit_fermions.transpiler.passes.QDriftTrotterization.html.md#qiskit_fermions.transpiler.passes.QDriftTrotterization).

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

* **Parameters:**
  * **num_terms** ([*int*](https://docs.python.org/3/builtins/functions.html#int)) – the number of terms to sample for the qDRIFT Trotterization. This equals the
    number of [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution) gates emitted per input gate; a larger value reduces
    the Trotterization error at the cost of a deeper circuit.
  * **weights** (*Sequence* *[*[*float*](https://docs.python.org/3/builtins/functions.html#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`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.weights) for the expected length, the sign convention and the effect on the
    evolution time.
  * **rng** (*np.random.Generator* *|* [*int*](https://docs.python.org/3/builtins/functions.html#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**](https://docs.python.org/3/builtins/exceptions.html#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](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html#numpy.ndarray) | [None](https://docs.python.org/3/builtins/constants.html#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()`](qiskit_fermions.operators.terms.group_coeff_means.html.md#qiskit_fermions.operators.terms.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`](qiskit_fermions.operators.OperatorTrait.html.md#qiskit_fermions.operators.OperatorTrait.groups), and per *term* otherwise; a
mismatch raises [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#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`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.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`](qiskit_fermions.transpiler.passes.F2FSynthesis.html.md#qiskit_fermions.transpiler.passes.F2FSynthesis) suits a circuit with a single
[`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.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](../guides/grouping.html.md#grouping-explanation), and
[`groups_are_hermitian()`](qiskit_fermions.operators.terms.groups_are_hermitian.html.md#qiskit_fermions.operators.terms.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*](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution)) – the gate to sample.
* **Returns:**
  The sampler, from which gates can be drawn in a batch or one at a time.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if [`weights`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.weights) was supplied and its length does not match the number of
      groups (or terms) of the operator of `gate`.
* **Return type:**
  [QDriftSampler](qiskit_fermions.circuit.library.synthesis.QDriftSampler.html.md#qiskit_fermions.circuit.library.synthesis.QDriftSampler)

#### synthesize(gate)

Synthesizes the gate into `num_terms` sampled gates.

* **Parameters:**
  **gate** ([*Evolution*](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution)) – the gate to synthesize.
* **Returns:**
  A [`FermionicCircuit`](qiskit_fermions.circuit.FermionicCircuit.html.md#qiskit_fermions.circuit.FermionicCircuit) holding one atomic [`Evolution`](qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution) gate per draw.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – if [`weights`](#qiskit_fermions.circuit.library.synthesis.FermionicQDrift.weights) was supplied and its length does not match the number of
      groups (or terms) of the operator of `gate`.
* **Return type:**
  [FermionicCircuit](qiskit_fermions.circuit.FermionicCircuit.html.md#qiskit_fermions.circuit.FermionicCircuit)
