Variational quantum circuits with the surrogate propagator¶
The surrogate propagators compile
a parameterized circuit into a SurrogateModel once. After that, evaluating the
expectation value for a new set of parameters is a cheap classical polynomial evaluation,
with no re-propagation needed. This makes them a natural fit for variational workflows
(VQE-style optimization loops), where the same ansatz gets evaluated at many different
parameter settings.
This notebook builds a small parameterized ansatz directly with Qiskit Parameters,
compiles it into a surrogate model, and optimizes its parameters with
scipy.optimize.minimize to minimize the expectation value of an observable. We then
validate the result by binding the optimized parameters back into the original Qiskit
circuit and comparing against an exact statevector simulation.
Building a parameterized ansatz¶
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import XXPlusYYGate
from qiskit.quantum_info import SparsePauliOp, Statevector
from scipy.optimize import minimize
n_qubits = 3
reps = 2
theta = ParameterVector("theta", n_qubits * reps)
phi = ParameterVector("phi", (n_qubits - 1) * reps)
qc = QuantumCircuit(n_qubits)
# HF State
qc.x(0)
qc.x(2)
it, ip = 0, 0
for layer in range(reps):
for q in range(n_qubits):
qc.rz(theta[it], q)
it += 1
for q in range(n_qubits - 1):
qc.append(XXPlusYYGate(phi[ip], 0.0), [q, q + 1])
ip += 1
qc.draw()
┌───┐ ┌──────────────┐┌────────────────────┐»
q_0: ─────┤ X ├──────┤ Rz(theta[0]) ├┤0 ├»
┌────┴───┴─────┐└──────────────┘│ (XX+YY)(phi[0],0) │»
q_1: ┤ Rz(theta[1]) ├────────────────┤1 ├»
└────┬───┬─────┘┌──────────────┐└────────────────────┘»
q_2: ─────┤ X ├──────┤ Rz(theta[2]) ├──────────────────────»
└───┘ └──────────────┘ »
« ┌──────────────┐ ┌────────────────────┐»
«q_0: ───┤ Rz(theta[3]) ├───────────────────┤0 ├»
« ┌──┴──────────────┴──┐┌──────────────┐│ (XX+YY)(phi[2],0) │»
«q_1: ┤0 ├┤ Rz(theta[4]) ├┤1 ├»
« │ (XX+YY)(phi[1],0) │├──────────────┤└────────────────────┘»
«q_2: ┤1 ├┤ Rz(theta[5]) ├──────────────────────»
« └────────────────────┘└──────────────┘ »
«
«q_0: ──────────────────────
« ┌────────────────────┐
«q_1: ┤0 ├
« │ (XX+YY)(phi[3],0) │
«q_2: ┤1 ├
« └────────────────────┘
We'll use a small antiferromagnetic-like $ZZ$ Hamiltonian as the observable, and compute its exact minimum eigenvalue directly as the ground truth.
observable = SparsePauliOp.from_list([("ZZI", 1.0), ("IZZ", 1.0), ("ZIZ", 0.5)])
exact_min_eigenvalue = np.linalg.eigvalsh(observable.to_matrix()).min()
print("Exact minimum eigenvalue:", exact_min_eigenvalue)
Exact minimum eigenvalue: -1.5
Let's build the surrogate propagator -
from propaq.circuits import SurrogatePauliCircuit
from propaq.datatypes import PauliTermSum
from propaq.propagators import PauliSurrogatePropagator
from propaq.models import VariationalSurrogateModel
obs_term_sum = PauliTermSum.from_sparse_pauli_op(observable)
surrogate_circuit = SurrogatePauliCircuit.from_qiskit(qc)
print("Qiskit circuit parameters:", qc.num_parameters)
print("propaq surrogate parameter slots:", surrogate_circuit.n_params)
model = PauliSurrogatePropagator().build(obs_term_sum, surrogate_circuit, initial_state=0)
variational_model = VariationalSurrogateModel(
model, surrogate_circuit.parameter_sources, surrogate_circuit.qiskit_parameters
)
Qiskit circuit parameters: 10 propaq surrogate parameter slots: 10
The cost function is just variational_model.evaluate(x).
def cost(x):
return variational_model.evaluate(x)
rng = np.random.default_rng(42)
x0 = rng.uniform(-np.pi, np.pi, size=len(variational_model.parameters))
print("Initial cost:", cost(x0))
result = minimize(cost, x0, method="COBYLA", options={"maxiter": 2000, "tol": 1e-10})
print("Optimized cost:", result.fun)
print("Optimizer success:", result.success)
print("Gap to exact minimum eigenvalue:", result.fun - exact_min_eigenvalue)
Initial cost: -0.7412010213855494
Optimized cost: -1.4999999999999845 Optimizer success: True Gap to exact minimum eigenvalue: 1.554312234475219e-14
Validating against an exact statevector simulation¶
Finally, we bind the optimized parameters back into the original Qiskit circuit and compute
the expectation value exactly via Statevector, to confirm it matches what the surrogate
model predicted.
binding = dict(zip(variational_model.parameters, result.x))
bound_qc = qc.assign_parameters(binding)
exact_ev = Statevector(bound_qc).expectation_value(observable).real
surrogate_ev = variational_model.evaluate(result.x)
print("Exact expectation value: ", exact_ev)
print("Surrogate expectation value: ", surrogate_ev)
print("Match:", np.isclose(exact_ev, surrogate_ev, atol=1e-8))
Exact expectation value: -1.4999999999999836 Surrogate expectation value: -1.4999999999999845 Match: True
Surrogate models have their own truncation policy, as well as unique logging fields! See the documentation on surrogate propagators for more information.