| name | qdrift |
| description | QDrift randomized Hamiltonian simulation, approximating e^{-iHt} by stochastically sampling Pauli-term evolutions with probability proportional to coefficient magnitude. |
QDrift Hamiltonian Simulation Skill Guide
Overview
QDrift is a randomized Hamiltonian simulation method for approximating
$$
U(t)=e^{-iHt}
$$
using randomly sampled short Pauli evolutions.
Key Insight
Instead of applying every Pauli term each slice, QDrift samples a single term at each step with probability proportional to coefficient magnitude, then applies a uniformly scaled angle. This replaces deterministic Trotter ordering with stochastic averaging.
Why QDrift Matters:
- Sampling-based structure can reduce dependence on number of Hamiltonian terms in some regimes.
- It provides a practical randomized baseline against deterministic product formulas.
- It is straightforward to implement and easy to parallelize over repeated trials.
- It naturally supports statistical error analysis through multiple seeds.
Real Applications:
- Sparse or large-term-count Hamiltonian simulation studies.
- Monte Carlo style benchmarking across random trajectories.
- Resource tradeoff experiments under depth constraints.
- Comparative studies with Trotter/Taylor/QSP pipelines.
Learning Objectives
After using this skill, you should be able to:
- Derive and implement QDrift sampling probabilities.
- Understand how
steps controls variance and depth.
- Use reproducible random seeds for fair comparisons.
- Interpret matrix-level outputs and spectral-norm error.
- Build aggregate statistics over repeated runs.
Prerequisites
Essential knowledge:
- Pauli decomposition of Hermitian matrices.
- Basic probability distributions and random sampling.
- Quantum gate-sequence execution concepts.
Mathematical comfort:
- Expectation and variance basics.
- Norm-based approximation error metrics.
- Scaling intuition with sample count.
Using the Provided Implementation
Quick Start Example
import numpy as np
from unitarylab_algorithms import QDriftAlgorithm
H = np.array([[2, 1],
[1, 3]], dtype=float)
algo = QDriftAlgorithm()
result = algo.run(
H=H,
t=1.0,
error=1e-8,
steps=5000,
backend='torch',
)
print("status:", result['status'])
print("Frobenius norm of error:", result['Frobenius norm of error'])
for f in result['plot']:
print(f"Saved {f['format']} file: {f['filename']}")
Core Parameters Explained
Constructor
class QDriftAlgorithm:
def __init__(self, text_mode: str = "plain", algo_dir: str = None) -> None:
...
| Parameter | Type | Default | Description |
|---|
text_mode | str | "plain" | Output text format mode. |
algo_dir | str|None | None | Directory for saving result files. Auto-generated if None. |
run() Parameters
def run(self, H: np.ndarray, t: float, error: float, steps: int = 5000, backend='torch', device='cpu', dtype=np.complex128):
...
| Parameter | Type | Default | Description |
|---|
H | np.ndarray | required | Hermitian Hamiltonian matrix (2D square array). Non-power-of-2 dimensions are zero-padded. |
t | float | required | Total evolution time. |
error | float | required | Desired approximation error (currently reserved; does not auto-set steps). |
steps | int | 5000 | Number of random Pauli samples. Larger values reduce variance and increase circuit depth. |
backend | str | 'torch' | Simulation backend for qc.get_matrix(). |
device | str | 'cpu' | Device for backend computation. |
dtype | type | np.complex128 | Dtype for matrix computation. |
Return Fields
The run() method returns a dictionary built by _build_return_dict(success, circuit_path, filepath, circuit). The self.output fields are merged into the result via result.update(self.output), so all keys below are accessible directly on the returned dict:
| Key | Type | Description |
|---|
status | str | 'ok' on success, 'failed' otherwise. |
circuit_path | str | Local path to the saved circuit diagram (SVG). |
plot | list | List of saved result files, each as {"format": str, "filename": str} (format is the 3-char file extension). |
circuit | Circuit | The assembled QDrift circuit object. |
Approximate evolution matrix | np.ndarray | Approximate unitary $U_{\text{approx}}$ from the random QDrift circuit (qc.get_matrix()). |
Exact evolution matrix | np.ndarray | Exact reference unitary $e^{-iHt}$ computed via scipy.linalg.expm. |
Frobenius norm of error | float | $|U_{\text{approx}} - U_{\text{exact}}|_F$ — Frobenius norm of the difference. |
Understanding the Core Components
1) Probability and angle construction
From _expand(decomposition, t, steps) in algorithm.py:
pauli_strings = [p for p, _ in decomposition]
coeffs = np.array([c.real for _, c in decomposition], dtype=float)
lam = np.sum(np.abs(coeffs))
probs = np.abs(coeffs) / lam
indices = np.random.choice(len(decomposition), size=steps, p=probs)
sequence = []
for idx in indices:
pauli_str = pauli_strings[idx]
sign = np.sign(coeffs[idx])
angle = sign * lam * t / steps
sequence.append((pauli_str, angle))
Interpretation:
- Probability mass follows coefficient magnitude.
- Every sampled gate uses same magnitude scale
lam * t / steps, modulated by sign.
np.random.choice(..., p=probs) is the stochastic core of QDrift.
2) Circuit assembly from random sequence
From run() in algorithm.py:
decomposition = pauli_string_decomposition(H)
sequence = self._expand(decomposition, t, steps)
reg = Register('K', n)
qc = Circuit(reg, name='QDrift Decomposition')
for pauli_str, angle in sequence:
gate = pauli_string_evolution(pauli_str, angle)
qc.append(gate, range(n))
U_approx = qc.get_matrix(backend=backend, device=device, dtype=dtype)
U_exact = expm(-1j * H * t)
U_error = norm(U_approx - U_exact, ord='fro')
Interpretation:
- The evolution result corresponds to one random trajectory.
np.random.choice is unseeded by default; results vary between runs.
- For stable evaluation, fix a random seed externally via
np.random.seed(...) before calling run().
3) External package function notes (brief)
pauli_string_decomposition from unitarylab.library.pauli_operator decomposes the matrix into Pauli terms.
pauli_string_evolution creates the gate implementation for each sampled term.
- Error is assessed via Frobenius norm against
scipy.linalg.expm(-1j * H * t).
Hands-On Example: Hamiltonian Simulation
Measure variance across random seeds and step counts.
import numpy as np
from unitarylab_algorithms import QDriftAlgorithm
XX = np.array([[0,0,0,1],[0,0,1,0],[0,1,0,0],[1,0,0,0]], dtype=float)
ZZ = np.array([[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]], dtype=float)
H = XX + ZZ
for steps in [1000, 5000]:
for seed in [0, 1, 2]:
np.random.seed(seed)
algo = QDriftAlgorithm()
result = algo.run(
H=H,
t=1.0,
error=1e-8,
steps=steps,
backend='torch',
)
err = result['Frobenius norm of error']
print(f"steps={steps}, seed={seed}, Frobenius error={err:.3e}")
What to look for:
- Larger
steps reduces the Frobenius-norm error on average.
- Different seeds produce different random trajectories with some variance.
Mathematical Deep Dive
For positive-coefficient decomposition,
$$
H = \sum_{\ell=1}^{L} h_\ell H_\ell,
\quad
h_\ell > 0,
\quad
\lambda = \sum_{\ell=1}^{L} h_\ell.
$$
Sampling probabilities are
$$
p_\ell = \frac{h_\ell}{\lambda}.
$$
The equivalent generalized form used in code is:
$$
H = \sum_{j=1}^{L} c_j P_j
$$
with
$$
\lambda = \sum_{j=1}^{L} |c_j|,
\quad
p_j = \frac{|c_j|}{\lambda}
$$
Set
$$
t_{\mathrm{step}} = \lambda t / N
$$
and build the random product
$$
U_{\text{qdrift}}(t)=\prod_{k=1}^{N}\exp(-iH_{j_k}t_{\mathrm{step}}).
$$
For signed coefficients in this implementation, each sampled gate is
$$
e^{-i,\mathrm{sign}(c_j),\lambda t / N; P_j}
$$
for total N = steps samples.
A commonly cited complexity expression in this randomized setting is
$$
N = O\left(\frac{(\lambda t)^2}{\epsilon}\right),
$$
while first-order deterministic product formulas are often compared using
$$
O\left(\frac{L^2(\lambda t)^2}{\epsilon}\right)
$$
style dependence.
Practical interpretation:
- One trajectory is random and biased toward large-magnitude terms.
- Increasing
N improves concentration around target evolution.
- Empirical error assessment should include repeated seeds and statistics.
Implementation-consistent notes:
- The positive-coefficient presentation $H=\sum h_j H_j$ is a special case. Current code supports signed real Pauli coefficients by sampling with $|c_j|$ and encoding sign in the rotation angle.
- The common statement "no explicit dependence on $L$" refers to the randomized quantum-step complexity form. In practical implementation, classical decomposition and sampling still iterate across terms.
- In this code path,
target_error is not used to automatically choose steps; users should tune steps experimentally.
Real-World Applications
- Noisy simulation pipelines where randomized depth patterns are useful.
- Fast baseline approximations for large Pauli expansions.
- Statistical benchmarking for algorithm selection.
- Randomized circuit studies in NISQ-style analyses.