Quick Start Guide

This guide shows how to compute the QGT, McLachlan gradients and the energy variance of a parameterized circuit.

Basic Usage

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.quantum_info import SparsePauliOp
from bowtie_qgt.bowtieqgt import BowtieQGT

num_qubits = 4
qc = QuantumCircuit(num_qubits)
params = ParameterVector("θ", 8)
for i in range(num_qubits):
    qc.ry(params[i], i)
for i in range(num_qubits - 1):
    qc.cx(i, i + 1)
for i in range(num_qubits):
    qc.ry(params[i + 4], i)

hamiltonian = SparsePauliOp.from_list(
    [("ZIII", -1.0), ("IZII", -1.0), ("IIZI", -1.0), ("IIIZ", -1.0), ("XXII", -0.5)]
)

bowtie = BowtieQGT(qc, hamiltonian)
generalized_qgt, energy = bowtie.evaluate([0.1] * qc.num_parameters)

qgt = bowtie.extract_qgt(generalized_qgt)  # real part by default
b_real = bowtie.extract_gradient(generalized_qgt, which="real_time")
b_imag = bowtie.extract_gradient(generalized_qgt, which="imag_time")
variance = bowtie.extract_variance(generalized_qgt)

evaluate accepts either a sequence of values ordered like qc.parameters or a {Parameter: value} mapping. Every parameter must be bound.

Supported Circuits

Every parameter must be the bare angle of exactly one rx, ry, rz, rxx, ryy or rzz gate. Any non-parameterized gates are allowed. The constructor raises a ValueError for parameter expressions (2*θ), other parameterized gates, a parameter that appears in several gates, classical bits, control flow, or a parameterized global phase.

Identity terms in the Hamiltonian are allowed: they shift the energy and nothing else.

Real and Imaginary Time

The same BowtieQGT object serves both. You only choose the direction when you extract the gradient:

Real time (VarQRTE)

Imaginary time (VarQITE)

Target

\(|\psi(t)\rangle = e^{-iHt}|\psi_0\rangle\)

\(|\psi(\tau)\rangle \propto e^{-H\tau}|\psi_0\rangle\)

Use

Quantum dynamics

Ground-state preparation

Call

extract_gradient(g, which="real_time")

extract_gradient(g, which="imag_time")

\(b_i\)

\(\mathrm{Im}(\langle\partial_i\psi|H|\psi\rangle - \langle\partial_i\psi|\psi\rangle E)\)

\(-\mathrm{Re}\langle\partial_i\psi|H|\psi\rangle = -\tfrac12\partial_i E\)

McLachlan’s variational principle gives \(\mathrm{Re}(G)\,\dot\theta = b\) in both cases. The QGT is the same; only \(b\) changes:

qgt = bowtie.extract_qgt(generalized_qgt)

# Real time: follow e^{-iHt}
b = bowtie.extract_gradient(generalized_qgt, which="real_time")
theta_dot = np.linalg.solve(qgt + 1e-6 * np.eye(len(b)), b)

# Imaginary time: flow towards the ground state
b = bowtie.extract_gradient(generalized_qgt, which="imag_time")
theta_dot = np.linalg.solve(qgt + 1e-6 * np.eye(len(b)), b)

The names refer to the evolution, not to the part of the matrix that is taken: the real-time gradient happens to be the real part of the mixed block of the generalized QGT, but in terms of the raw overlap \(\langle\partial_i\psi|H|\psi\rangle\) it is the imaginary part. which= hides this bookkeeping. Examples has a full run of both.

Choosing the Simulator

BowtieQGT runs the bowtie circuits on a default statevector AerSimulator. Choose the device with "CPU" (default) or "GPU":

bowtie = BowtieQGT(qc, hamiltonian)                   # CPU
bowtie = BowtieQGT(qc, hamiltonian, simulator="GPU")  # needs qiskit-aer-gpu

For anything else (precision, number of threads, …), pass your own noiseless statevector simulator:

from qiskit_aer import AerSimulator

simulator = AerSimulator(method="statevector", precision="single", max_parallel_threads=8)
bowtie = BowtieQGT(qc, hamiltonian, simulator=simulator)

Progress and Timings

verbosity=1 shows progress bars while building the bowties and prints their light-cone widths. verbosity=2 also reports every evaluate call. The time spent in the last call is always available:

bowtie.evaluate(values)
bowtie.last_timings  # {"simulation": seconds, "assembly": seconds}

Understanding the Output

The generalized QGT is the connected QGT of the circuit extended by one first-order Trotter step \(e^{-i c_j h_j t}\) per Hamiltonian term. It is complex Hermitian, with N circuit parameters followed by M non-identity Hamiltonian terms:

┌─────────────┬──────────────┐
│     QGT     │    mixed     │
│   (N×N)     │    (N×M)     │
├─────────────┼──────────────┤
│   mixed†    │ Hamiltonian  │
│   (M×N)     │    (M×M)     │
└─────────────┴──────────────┘

The parameter tangents are \(|\partial_i\psi\rangle\) and the Hamiltonian tangents are \(-i h_j|\psi\rangle\). For \(H = \sum_j c_j h_j\):

\[\begin{split}G_{ij} &= \langle\partial_i\psi|\partial_j\psi\rangle - \langle\partial_i\psi|\psi\rangle\langle\psi|\partial_j\psi\rangle \\ b^{\text{real}}_i + i\, b^{\text{imag}}_i &= \sum_j \tilde G_{i, N+j}\, c_j \\ \mathrm{Var}[H] &= \sum_{jk} c_j^*\, \tilde G_{N+j, N+k}\, c_k\end{split}\]

with \(b^{\text{real}}_i = \mathrm{Im}(\langle\partial_i\psi|H|\psi\rangle - \langle\partial_i\psi|\psi\rangle E)\) and \(b^{\text{imag}}_i = -\mathrm{Re}(\langle\partial_i\psi|H|\psi\rangle - \langle\partial_i\psi|\psi\rangle E)\). For Hermitian \(H\) the last term is real and drops out, so \(b^{\text{imag}} = -\tfrac12\nabla_\theta E\).

extract_qgt returns \(\mathrm{Re}\,G\) by default, the Fubini-Study metric that McLachlan’s principle uses. part="imag" gives the Berry curvature and part="complex" the full QGT, for other variational principles. Rotations follow Qiskit’s convention \(e^{-i\theta P/2}\), so G matches qiskit_algorithms.gradients.ReverseQGT.

Next Steps