Variational Quantum Circuits
Variational quantum circuits (VQCs) are parameterized quantum circuits whose gate parameters can be optimized classically. They form the backbone of variational quantum algorithms like VQE and QAOA.
What Are Variational Circuits?
A variational quantum circuit consists of:
- Parameterized gates: Quantum gates with tunable parameters (e.g., rotation angles)
- Cost function: An observable (typically a Hamiltonian) to minimize
- Classical optimizer: Updates gate parameters based on measured cost
The optimization loop:
sequenceDiagram
participant C as Classical Optimizer
participant Q as Quantum Circuit
loop Until convergence
C->>Q: Set parameters θ
Q->>Q: Prepare state |ψ(θ)⟩
Q->>C: Measure ⟨H⟩ = ⟨ψ(θ)|H|ψ(θ)⟩
C->>C: Compute gradients ∂⟨H⟩/∂θᵢ
C->>C: Update θ ← θ - η∇⟨H⟩
end
Note over C,Q: Return optimal θ* and ⟨H(θ*)⟩The VQCircuit Module
The vqcircuit module provides tools for building and optimizing parameterized circuits:
from pyqpanda3 import vqcircuitKey Components
| Component | Description |
|---|---|
VQCircuit | Parameterized quantum circuit builder |
Parameter | Parameter container for variational parameters |
DiffMethod | Differentiation method (ADJOINT_DIFF) |
ResGradients | Gradient values for single parameter set |
ResNGradients | Gradient values for N parameter sets |
ResGradientsAndExpectation | Combined gradients and expectation value |
VQCircuit Class API
The VQCircuit class is the central interface for constructing, evaluating, and differentiating parameterized quantum circuits. This section documents every public method with usage examples.
Constructor
from pyqpanda3 import vqcircuit, core
# Create a VQCircuit (pre_split defaults to True)
vqc = vqcircuit.VQCircuit()The constructor accepts an optional bool pre_split parameter (defaults to True), which controls whether the circuit is pre-split for gradient computation.
Adding Gates
Variational gates are appended to the circuit using the standard << operator, just like building an ordinary QProg. The difference is that parameter indices (of type ParamExpression) are supplied instead of numeric angles.
from pyqpanda3 import core, vqcircuit
# Build a single-layer variational circuit on 2 qubits
# Param() is a method on VQCircuit that returns parameter references
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4]) # define 4 mutable parameters
param_theta_0 = vqc.Param([0]) # parameter index 0
param_theta_1 = vqc.Param([1]) # parameter index 1
param_phi_0 = vqc.Param([2]) # parameter index 2
param_phi_1 = vqc.Param([3]) # parameter index 3
vqc << core.RY(0, param_theta_0)
vqc << core.RY(1, param_theta_1)
vqc << core.CNOT(0, 1)
vqc << core.RY(0, param_phi_0)
vqc << core.RY(1, param_phi_1)Setting Parameter Values
Before computing expectations or gradients, you assign concrete numeric values to the parameter slots:
from pyqpanda3 import vqcircuit
import numpy as np
# Assign values to all parameters at once via __call__
theta = np.array([0.5, 1.2, 0.8, 2.1])
result = vqc(theta) # returns VQCResultComputing Expectation Values
The expectation value of a Hamiltonian with respect to the parameterized state is obtained through the simulation utilities. Using a PauliOperator as the observable:
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
# Define a simple Z-Z Hamiltonian on 2 qubits
hamiltonian = PauliOperator({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
# Build the quantum program
prog = core.QProg()
prog << core.RY(0, 0.5) << core.RY(1, 1.2)
prog << core.CNOT(0, 1)
prog << core.RY(0, 0.8) << core.RY(1, 2.1)
# Compute the expectation value (statevector backend)
exp_val = core.expval_pauli_operator(prog, hamiltonian)
print(f"Expectation value: {exp_val:.6f}")运行结果:
Expectation value: -0.466581Computing Gradients
Gradients are computed with respect to every parameter using the adjoint differentiation method:
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import Hamiltonian
import numpy as np
# Create a 2-qubit variational circuit with 4 parameters
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4])
p0 = vqc.Param([0])
p1 = vqc.Param([1])
p2 = vqc.Param([2])
p3 = vqc.Param([3])
vqc << core.RY(0, p0) << core.RY(1, p1)
vqc << core.CNOT(0, 1)
vqc << core.RY(0, p2) << core.RY(1, p3)
# Define the Hamiltonian (using Hamiltonian type for VQCircuit API)
hamiltonian = Hamiltonian({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
# Compute gradients using adjoint differentiation
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
# Assign concrete parameter values
theta = np.array([0.5, 1.2, 0.8, 2.1], dtype=np.float64)
# The gradient API is an instance method on VQCircuit
grad_result = vqc.get_gradients(theta, hamiltonian, diff_method)
# Access individual gradient values
for i in range(len(grad_result)):
g = grad_result.at(i)
print(f" dE/dtheta[{i}] = {g:.6f}")运行结果:
dE/dtheta[0] = 0.378081
dE/dtheta[1] = 0.104312
dE/dtheta[2] = 0.455621
dE/dtheta[3] = -0.045044Computing Gradients and Expectation Together
For efficiency, gradients and the expectation value can be computed in a single pass:
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import Hamiltonian
import numpy as np
# Build the same VQCircuit and Hamiltonian as the gradient example
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4])
p0, p1, p2, p3 = vqc.Param([0]), vqc.Param([1]), vqc.Param([2]), vqc.Param([3])
vqc << core.RY(0, p0) << core.RY(1, p1) << core.CNOT(0, 1)
vqc << core.RY(0, p2) << core.RY(1, p3)
hamiltonian = Hamiltonian({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
theta = np.array([0.5, 1.2, 0.8, 2.1], dtype=np.float64)
# Obtain both gradients and expectation in one evaluation
combined = vqc.get_gradients_and_expectation(theta, hamiltonian, diff_method)
print(f"Expectation: {combined.expectation_val():.6f}")
print(f"Gradients: {combined.gradients()}")运行结果:
Expectation: -0.466581
Gradients: [0.37808143066521327, 0.1043124235438834, 0.45562134087720296, -0.0450441769683861]Batch Evaluation with ResNGradients
When evaluating gradients for multiple parameter sets at once, the ResNGradients container is returned:
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import Hamiltonian
import numpy as np
# Create a variational circuit with 4 parameters
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4])
p0, p1, p2, p3 = vqc.Param([0]), vqc.Param([1]), vqc.Param([2]), vqc.Param([3])
vqc << core.RY(0, p0) << core.RY(1, p1) << core.CNOT(0, 1)
vqc << core.RY(0, p2) << core.RY(1, p3)
# Define the Hamiltonian
hamiltonian = Hamiltonian({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
# Specify the differentiation method
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
# Evaluate gradients for a batch of parameter configurations
# Flatten all parameter sets into a single 1D array
batch_size = 3
theta_flat = np.array([
0.1, 0.2, 0.3, 0.4,
0.5, 0.6, 0.7, 0.8,
1.0, 1.1, 1.2, 1.3,
], dtype=np.float64)
# Use the batch overload with param_group_total
n_grad_result = vqc.get_gradients(theta_flat, hamiltonian, batch_size, diff_method)
print(f"Number of parameter sets evaluated: {len(n_grad_result)}")
for i in range(len(n_grad_result)):
grad_set = n_grad_result.at(i)
print(f" Set {i} gradients: {grad_set.gradients()}")运行结果:
Number of parameter sets evaluated: 3
Set 0 gradients: [0.0179528385604528, -0.721177868772871, -0.3633729025298299, -0.6801970817746747]
Set 1 gradients: [0.1284286224925652, -1.1069208263697317, -0.2645958714670071, -0.7755908491348013]
Set 2 gradients: [-5.56234512694605e-310, 4.959947347649675e-270, -7.65e-321, 4.959947347649675e-270]API Method Summary
| Method | Signature | Return Type | Description |
|---|---|---|---|
| Constructor | VQCircuit(pre_split=True) | VQCircuit | Create circuit (pre_split controls gradient pre-computation) |
| Add gate | vqc << gate | VQCircuit | Append a variational gate to the circuit |
| Param ref | vqc.Param([idx]) | ParamExpression | Get parameter reference for gate construction |
| Gradients | vqc.get_gradients(theta, H, method) | ResGradients | Compute gradients of |
| Batch gradients | vqc.get_gradients(theta, H, n, method) | ResNGradients | Gradients for n parameter sets |
| Combined | vqc.get_gradients_and_expectation(theta, H, method) | ResGradientsAndExpectation | Gradients and expectation together |
Variational Gates (VQGate)
Variational gates are parameterized gate wrappers. They accept ParamExpression objects instead of fixed angles, allowing parameters to be updated during optimization.
VQGate functions are exported from the core module when called with ParamExpression parameters:
Single-Qubit Parameterized Gates
| Gate | Signature | Description |
|---|---|---|
RX(qubit, param) | core.RX(idx, ParamExpression) | RX rotation with parameter |
RY(qubit, param) | core.RY(idx, ParamExpression) | RY rotation with parameter |
RZ(qubit, param) | core.RZ(idx, ParamExpression) | RZ rotation with parameter |
P(qubit, param) | core.P(idx, ParamExpression) | Phase gate with parameter |
U1(qubit, param) | core.U1(idx, ParamExpression) | U1 gate with parameter |
U2(qubit, p1, p2) | core.U2(idx, param1, param2) | U2 with 2 parameters |
U3(qubit, p1, p2, p3) | core.U3(idx, p1, p2, p3) | U3 with 3 parameters |
U4(qubit, p1, p2, p3, p4) | core.U4(idx, p1, p2, p3, p4) | U4 with 4 parameters |
RPHI(qubit, p1, p2) | core.RPhi(idx, param1, param2) | RPhi with 2 parameters |
Two-Qubit Parameterized Gates
| Gate | Signature | Description |
|---|---|---|
CU(ctrl, targ, p1-p4) | core.CU(ctrl, targ, ...) | Controlled-U with 4 params |
CP(ctrl, targ, param) | core.CP(ctrl, targ, param) | Controlled-phase |
CRX(ctrl, targ, param) | core.CRX(ctrl, targ, param) | Controlled-RX |
CRY(ctrl, targ, param) | core.CRY(ctrl, targ, param) | Controlled-RY |
CRZ(ctrl, targ, param) | core.CRZ(ctrl, targ, param) | Controlled-RZ |
RXX(ctrl, targ, param) | core.RXX(ctrl, targ, param) | RXX rotation |
RYY(ctrl, targ, param) | core.RYY(ctrl, targ, param) | RYY rotation |
RZZ(ctrl, targ, param) | core.RZZ(ctrl, targ, param) | RZZ rotation |
RZX(ctrl, targ, param) | core.RZX(ctrl, targ, param) | RZX rotation |
Mixed Fixed and Variational Parameters
Multi-parameter gates support mixing fixed values with variational parameters:
from pyqpanda3 import core, vqcircuit
# Create a VQCircuit and get a variational parameter reference
vqc = vqcircuit.VQCircuit()
vqc.set_Param([1]) # define 1 mutable parameter
param_index = vqc.Param([0])
# U3 with first two params fixed, third variational
gate = core.U3(0, 1.57, 0.0, param_index)
# U4 with only last param variational
gate = core.U4(0, 0.0, 0.0, 0.0, param_index)
# U2 with first param variational, second fixed
gate = core.U2(0, param_index, 3.14)Parameter Management
Parameter Class
The Parameter class manages the variational parameters:
from pyqpanda3 import vqcircuit
# The Parameter class is typically used through VQCircuit
# VQCircuit manages parameters internally, mutable_parameter_total() returns the total count
vqc = vqcircuit.VQCircuit()
# Query the number of mutable parameters in the circuit (initially 0)
n = vqc.mutable_parameter_total()
print(f"Number of mutable parameters: {n}")运行结果:
Number of mutable parameters: 0ParamExpression
The ParamExpression specifies where a parameter value comes from:
- Index-based:
param = vqc.Param([idx_dim0, idx_dim1, ...])— references a multi-dimensional parameter - Scalar index:
param = vqc.Param([idx])— references a single parameter by index
from pyqpanda3 import core, vqcircuit
# Create a VQCircuit to hold parameters
vqc = vqcircuit.VQCircuit()
vqc.set_Param([2]) # define 2 mutable parameters
# Create parameterized RX gate using VQCircuit.Param()
# The ParamExpression specifies which parameter slot to use
param_0 = vqc.Param([0]) # References parameter at index 0
param_1 = vqc.Param([1]) # References parameter at index 1
# Append variational gates to the VQCircuit
vqc << core.RX(0, param_0)
vqc << core.RY(1, param_1)Gradient Computation
Adjoint Differentiation
The adjoint differentiation method efficiently computes gradients by evolving the circuit forward and then backward (adjoint). This avoids the
Reference: Jones and Gacon, "Efficient calculation of gradients in classical simulations of variational quantum algorithms", arXiv:2009.02823.
from pyqpanda3 import vqcircuit
# Specify the differentiation method
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFFResGradients - Single Parameter Set
ResGradients holds gradient values for a single set of parameters:
# Assume gradients is a ResGradients object from VQCircuit computation
try:
gradients = vqc_result.gradients() # Get ResGradients
except NameError:
print("(Define vqc_result by running a VQCircuit computation first.)")
gradients = None
if gradients is not None:
# Get number of gradients
n_grads = len(gradients)
# Get all gradient values as a list
all_grads = gradients.gradients()
# String representation
print(gradients)运行结果:
(Define vqc_result by running a VQCircuit computation first.)ResNGradients - N Parameter Sets
ResNGradients holds gradient values for N sets of parameters (batch evaluation):
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import Hamiltonian
import numpy as np
# Create a variational circuit for batch demonstration
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4])
p0, p1, p2, p3 = vqc.Param([0]), vqc.Param([1]), vqc.Param([2]), vqc.Param([3])
vqc << core.RY(0, p0) << core.RY(1, p1) << core.CNOT(0, 1)
vqc << core.RY(0, p2) << core.RY(1, p3)
hamiltonian = Hamiltonian({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
batch_size = 3
theta_flat = np.array([
0.1, 0.2, 0.3, 0.4,
0.5, 0.6, 0.7, 0.8,
1.0, 1.1, 1.2, 1.3,
], dtype=np.float64)
# n_gradients is a ResNGradients object from vqc.get_gradients(..., batch_size, ...)
n_gradients = vqc.get_gradients(theta_flat, hamiltonian, batch_size, diff_method)
# Get number of parameter sets (N)
n_sets = len(n_gradients)
# Get ResGradients for the first parameter set
grad_set_i = n_gradients.at(0)
# Get all gradient data as nested list
all_data = n_gradients.data()
# String representation
print(n_gradients)运行结果:
(gradient(double)):[{Parameter.at([0]):0.0179528,Parameter.at([1]):-0.721178,Parameter.at([2]):-0.363373,Parameter.at([3]):-0.680197},{Parameter.at([0]):0.128429,Parameter.at([1]):-1.10692,Parameter.at([2]):-0.264596,Parameter.at([3]):-0.775591},{Parameter.at([0]):-5.56235e-310,Parameter.at([1]):4.95995e-270,Parameter.at([2]):-7.64814e-321,Parameter.at([3]):4.95995e-270}]ResGradientsAndExpectation - Combined Result
ResGradientsAndExpectation holds both gradient values and the expectation value:
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import Hamiltonian
import numpy as np
# Create a variational circuit for demonstration
vqc = vqcircuit.VQCircuit()
vqc.set_Param([4])
p0, p1, p2, p3 = vqc.Param([0]), vqc.Param([1]), vqc.Param([2]), vqc.Param([3])
vqc << core.RY(0, p0) << core.RY(1, p1) << core.CNOT(0, 1)
vqc << core.RY(0, p2) << core.RY(1, p3)
hamiltonian = Hamiltonian({"Z0 Z1": 1.0, "Z0": 0.5, "Z1": 0.3})
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
theta = np.array([0.5, 1.2, 0.8, 2.1], dtype=np.float64)
# result is a ResGradientsAndExpectation object from vqc.get_gradients_and_expectation(...)
result = vqc.get_gradients_and_expectation(theta, hamiltonian, diff_method)
# Get the expectation value
exp_val = result.expectation_val()
# Get all gradient values as a list
grads = result.gradients()
# Get combined data: (expectation, [gradients])
data = result.data()
# String representation
print(result)运行结果:
(gradient and expectation){"expectation":-0.466581,"gradients":{Parameter.at([0]):0.378081,Parameter.at([1]):0.104312,Parameter.at([2]):0.455621,Parameter.at([3]):-0.0450442}}Mathematical Foundation
Parameterized Quantum State
A parameterized circuit
Expectation Value
The cost function is the expectation value of a Hamiltonian
Gradient via Adjoint Differentiation
The gradient with respect to parameter
where:
The adjoint method avoids storing intermediate states by computing the inner product during a single forward-backward pass.
Parameter Shift Rule (Alternative)
For comparison, the parameter shift rule computes each gradient using two circuit evaluations:
This requires
VQA Workflow
graph TD
A[Define Ansatz] --> B[Initialize Parameters θ]
B --> C[Build VQCircuit]
C --> D[Compute ⟨H⟩ and Gradients]
D --> E{Converged?}
E -->|No| F[Update Parameters]
F --> C
E -->|Yes| G[Return Optimal θ* and ⟨H*⟩]
subgraph "VQCircuit Step"
C --> C1[Set parameter values]
C1 --> C2[Build quantum circuit]
C2 --> C3[Run simulation]
C3 --> C4[Compute expectation]
C4 --> C5[Compute gradients]
C5 --> D
end
style A fill:#4CAF50,color:#fff
style G fill:#2196F3,color:#fff
style D fill:#FF9800,color:#fffBuilding a VQCircuit Step by Step
This section walks through the complete construction of a variational quantum circuit from scratch, explaining each decision along the way. We will build a 3-qubit circuit suitable for a small optimization problem.
Step 1: Decide on the Ansatz Structure
Before writing any code, decide what ansatz (circuit structure) is appropriate for your problem. For this walkthrough we use a hardware-efficient ansatz with alternating rotation and entangling layers.
graph TD
A["Choose number of qubits (n)"] --> B["Choose circuit depth (d)"]
B --> C["Count total parameters: n * 2 * d"]
C --> D["Allocate Parameter indices"]
D --> E["Build rotation layer"]
E --> F["Build entangling layer"]
F --> G{More layers?}
G -->|Yes| E
G -->|No| H["Circuit is ready"]
style A fill:#4CAF50,color:#fff
style H fill:#2196F3,color:#fffStep 2: Import Modules and Define Constants
from pyqpanda3 import core, vqcircuit
# Circuit configuration
N_QUBITS = 3 # Number of qubits
DEPTH = 2 # Number of repeated layers
# Each layer has 2 rotations per qubit (RY then RZ)
N_PARAMS = N_QUBITS * 2 * DEPTH # = 12 parameters totalStep 3: Create the Parameter Object
The Parameter object tracks all variational parameters and their current values.
from pyqpanda3 import vqcircuit
# Parameters are managed through VQCircuit
vqc = vqcircuit.VQCircuit()
n = vqc.mutable_parameter_total()
print(f"Initial parameter count: {n}")运行结果:
Initial parameter count: 0Step 4: Build the Ansatz Circuit
Construct the circuit layer by layer. Each layer consists of RY and RZ rotations on every qubit, followed by CNOT entangling gates.
def build_hardware_efficient_ansatz(n_qubits, depth):
"""
Construct a hardware-efficient ansatz with the given qubit count and depth.
Returns a QProg and the total number of parameters used.
"""
prog = core.QProg()
param_idx = 0 # running index for parameter allocation
for layer in range(depth):
# --- Rotation sub-layer ---
for q in range(n_qubits):
# RY rotation for qubit q (use 0.0 as placeholder)
prog << core.RY(q, 0.0)
param_idx += 1
# RZ rotation for qubit q (use 0.0 as placeholder)
prog << core.RZ(q, 0.0)
param_idx += 1
# --- Entangling sub-layer (linear nearest-neighbor) ---
for q in range(n_qubits - 1):
prog << core.CNOT(q, q + 1)
# Close the ring for full connectivity
if n_qubits > 2:
prog << core.CNOT(n_qubits - 1, 0)
total_params = param_idx
return prog, total_params
# Build the circuit
ansatz_prog, n_params_used = build_hardware_efficient_ansatz(N_QUBITS, DEPTH)
print(f"Circuit built with {n_params_used} parameters")运行结果:
Circuit built with 12 parametersStep 5: Define the Observable (Hamiltonian)
Choose a Hamiltonian to serve as the cost function. For demonstration we use a transverse-field Ising model:
from pyqpanda3.hamiltonian import PauliOperator
# Build the Hamiltonian as a PauliOperator
hamiltonian_terms = {}
# ZZ coupling terms (nearest neighbor on a ring)
for i in range(N_QUBITS):
j = (i + 1) % N_QUBITS
hamiltonian_terms[f"Z{i} Z{j}"] = -1.0
# Transverse field terms
h_field = 0.5
for i in range(N_QUBITS):
hamiltonian_terms[f"X{i}"] = -h_field
hamiltonian = PauliOperator(hamiltonian_terms)
print(f"Hamiltonian terms: {hamiltonian_terms}")运行结果:
Hamiltonian terms: {'Z0 Z1': -1.0, 'Z1 Z2': -1.0, 'Z2 Z0': -1.0, 'X0': -0.5, 'X1': -0.5, 'X2': -0.5}Step 6: Initialize Parameters
Set initial parameter values. A common choice is random angles uniformly distributed in
# Random initialization
np.random.seed(42)
initial_theta = np.random.uniform(0, 2 * np.pi, n_params_used)
print(f"Initial parameters: {initial_theta}")运行结果:
Initial parameters: [2.35330497 5.97351416 4.59925358 3.76148219 0.98029403 0.98014248
0.3649501 5.44234523 3.77691701 4.44895122 0.12933619 6.09412333]Step 7: Evaluate the Cost Function
def cost_function(theta_array):
"""Compute the expectation value of the Hamiltonian for given parameters."""
# Build a new circuit with the given parameter values
prog = core.QProg()
idx = 0
for layer in range(DEPTH):
for q in range(N_QUBITS):
prog << core.RY(q, theta_array[idx]); idx += 1
prog << core.RZ(q, theta_array[idx]); idx += 1
for q in range(N_QUBITS - 1):
prog << core.CNOT(q, q + 1)
if N_QUBITS > 2:
prog << core.CNOT(N_QUBITS - 1, 0)
# Evaluate expectation of the full Hamiltonian
return core.expval_pauli_operator(prog, hamiltonian)
# Test the cost function
energy_0 = cost_function(initial_theta)
print(f"Initial energy: {energy_0:.6f}")运行结果:
Initial energy: 0.857640Step 8: Run the Optimization Loop
from scipy.optimize import minimize
# Optimize with COBYLA (derivative-free)
result = minimize(
cost_function,
initial_theta,
method='COBYLA',
options={'maxiter': 300, 'rhobeg': 0.5}
)
print(f"Optimized energy: {result.fun:.6f}")
print(f"Optimal parameters: {result.x}")
print(f"Converged: {result.success}")
print(f"Function evaluations: {result.nfev}")运行结果:
Optimized energy: -3.122579
Optimal parameters: [2.89738707e+00 6.77262940e+00 6.03949134e+00 3.54978399e+00
7.03084001e-03 2.05729526e+00 5.93804168e-03 5.39264891e+00
3.03132810e+00 4.77970518e+00 3.14227331e+00 8.26800250e+00]
Converged: False
Function evaluations: 300Step 9: Verify the Result
After optimization, inspect the final quantum state and check convergence:
# Rebuild circuit at optimal parameters
optimal_prog = core.QProg()
idx = 0
for layer in range(DEPTH):
for q in range(N_QUBITS):
optimal_prog << core.RY(q, result.x[idx]); idx += 1
optimal_prog << core.RZ(q, result.x[idx]); idx += 1
for q in range(N_QUBITS - 1):
optimal_prog << core.CNOT(q, q + 1)
if N_QUBITS > 2:
optimal_prog << core.CNOT(N_QUBITS - 1, 0)
# Get the final state vector
machine = core.CPUQVM()
machine.run(optimal_prog, shots=1000)
final_state = machine.result().get_state_vector()
print(f"State vector norm: {np.linalg.norm(final_state):.6f}")
# Compute measurement probabilities
probs = np.abs(final_state) ** 2
for i, prob in enumerate(probs):
if prob > 0.01:
bitstring = format(i, f'0{N_QUBITS}b')
print(f" |{bitstring}>: {prob:.4f}")运行结果:
State vector norm: 1.000000
|000>: 0.9648
|010>: 0.0155
|100>: 0.0149Batch Parameter Evaluation
When exploring the parameter landscape or performing population-based optimization, it is efficient to evaluate gradients for multiple parameter sets simultaneously. The ResNGradients container is designed for this purpose.
Why Batch Evaluation?
Evaluating gradients for
- Grid search over a discrete parameter mesh
- Evolutionary algorithms that maintain a population of candidate solutions
- Parallel gradient comparison to identify the most promising descent direction
Batch Evaluation Workflow
graph LR
A["Prepare N<br>parameter sets"] --> B["Call<br>get_gradients(..., n)"]
B --> C["ResNGradients<br>container"]
C --> D["Iterate results"]
D --> E["Select best<br>descent direction"]
style A fill:#4CAF50,color:#fff
style C fill:#FF9800,color:#fff
style E fill:#2196F3,color:#fffExample: Evaluating a Parameter Grid
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
# --- Build a simple 2-qubit variational circuit ---
def build_2q_circuit(theta):
"""Build a 2-qubit circuit with 2 variational parameters."""
prog = core.QProg()
prog << core.RY(0, theta[0])
prog << core.RY(1, theta[1])
prog << core.CNOT(0, 1)
prog << core.RY(0, theta[0])
return prog
# Define the Hamiltonian (Z-Z interaction)
H = PauliOperator({"Z0 Z1": 1.0, "Z0": 0.5})
# --- Create a grid of parameter values ---
grid_size = 5
theta_0_vals = np.linspace(0, np.pi, grid_size)
theta_1_vals = np.linspace(0, np.pi, grid_size)
# Collect all parameter combinations
batch = []
for t0 in theta_0_vals:
for t1 in theta_1_vals:
batch.append([t0, t1])
print(f"Total parameter sets: {len(batch)}")运行结果:
Total parameter sets: 25Computing Batch Gradients
# Compute gradients for each parameter set
# Using the adjoint differentiation method
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFF
gradient_results = []
energies = []
for theta in batch:
prog = build_2q_circuit(theta)
# Compute expectation value
energy = core.expval_pauli_operator(prog, H)
energies.append(energy)
# For demonstration, compute parameter-shift gradients manually
shift = np.pi / 2
grads = []
for i in range(2):
# Forward shift
theta_plus = theta.copy()
theta_plus[i] += shift
prog_plus = build_2q_circuit(theta_plus)
e_plus = core.expval_pauli_operator(prog_plus, H)
# Backward shift
theta_minus = theta.copy()
theta_minus[i] -= shift
prog_minus = build_2q_circuit(theta_minus)
e_minus = core.expval_pauli_operator(prog_minus, H)
# Gradient via parameter shift rule
grad_i = 0.5 * (e_plus - e_minus)
grads.append(grad_i)
gradient_results.append(grads)
# Find the parameter set with the steepest descent
grad_norms = [np.linalg.norm(g) for g in gradient_results]
best_idx = np.argmin(energies)
print(f"Best energy: {energies[best_idx]:.6f} at params {batch[best_idx]}")
print(f"Gradient at best: {gradient_results[best_idx]}")
print(f"Grad norm: {grad_norms[best_idx]:.6f}")运行结果:
Best energy: -0.500000 at params [np.float64(1.5707963267948966), np.float64(1.5707963267948966)]
Gradient at best: [0.0, 0.0]
Grad norm: 0.000000Analyzing the Energy Landscape
# Reshape results into a 2D grid for visualization analysis
energy_grid = np.array(energies).reshape(grid_size, grid_size)
# Report the energy landscape statistics
print(f"Energy range: [{np.min(energy_grid):.6f}, {np.max(energy_grid):.6f}]")
print(f"Energy mean: {np.mean(energy_grid):.6f}")
print(f"Energy std: {np.std(energy_grid):.6f}")
# Identify local minima
flat_idx = np.argmin(energy_grid)
min_row, min_col = np.unravel_index(flat_idx, energy_grid.shape)
print(f"Grid minimum at theta_0={theta_0_vals[min_col]:.3f}, "
f"theta_1={theta_1_vals[min_row]:.3f}")
print(f"Minimum energy: {energy_grid[min_row, min_col]:.6f}")运行结果:
Energy range: [-0.500000, 1.500000]
Energy mean: 0.203431
Energy std: 0.670428
Grid minimum at theta_0=1.571, theta_1=1.571
Minimum energy: -0.500000Gradient Verification and Debugging
When a variational algorithm fails to converge, the gradient computation is often the culprit. This section describes strategies to verify gradient correctness and debug common issues.
Numerical Gradient Check
The most reliable verification method is to compare analytic gradients against a finite-difference approximation:
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
def numerical_gradient(cost_fn, theta, epsilon=1e-5):
"""
Compute numerical gradients via central finite differences.
Args:
cost_fn: Callable that takes a parameter array and returns a float.
theta: Current parameter values (numpy array).
epsilon: Finite difference step size.
Returns:
numpy array of gradient values, one per parameter.
"""
n = len(theta)
grads = np.zeros(n)
for k in range(n):
# Perturb parameter k forward
theta_plus = theta.copy()
theta_plus[k] += epsilon
f_plus = cost_fn(theta_plus)
# Perturb parameter k backward
theta_minus = theta.copy()
theta_minus[k] -= epsilon
f_minus = cost_fn(theta_minus)
# Central difference
grads[k] = (f_plus - f_minus) / (2 * epsilon)
return grads
# Example usage: verify adjoint gradients against numerical ones
def build_demo_circuit(theta):
prog = core.QProg()
prog << core.RY(0, theta[0])
prog << core.RZ(0, theta[1])
prog << core.RY(1, theta[2])
prog << core.CNOT(0, 1)
return prog
H_demo = PauliOperator({"Z0 Z1": 1.0, "X0": 0.3})
def demo_cost(theta):
prog = build_demo_circuit(theta)
return core.expval_pauli_operator(prog, H_demo)
# Test at a specific parameter point
theta_test = np.array([0.5, 1.0, 1.5])
num_grads = numerical_gradient(demo_cost, theta_test)
print("Numerical gradients:", num_grads)
print("Gradient magnitudes:", np.abs(num_grads))运行结果:
Numerical gradients: [ 0.14189163 -0.12072363 -0.99199797]
Gradient magnitudes: [0.14189163 0.12072363 0.99199797]Gradient Consistency Check
For a robust check, compare gradients at multiple parameter points:
def verify_gradients_at_point(theta, cost_fn, epsilon=1e-5, tol=1e-4):
"""
Verify that numerical gradients are self-consistent
at multiple epsilon values.
"""
# Compute at two different epsilon values
g1 = numerical_gradient(cost_fn, theta, epsilon=epsilon)
g2 = numerical_gradient(cost_fn, theta, epsilon=epsilon * 0.1)
# They should agree to within the tolerance
diff = np.max(np.abs(g1 - g2))
consistent = diff < tol
print(f" Epsilon={epsilon:.1e} vs {epsilon*0.1:.1e}: max diff = {diff:.2e}")
print(f" Consistent: {consistent}")
return consistent
# Verify at several random parameter points
np.random.seed(0)
for trial in range(5):
theta_random = np.random.uniform(0, 2 * np.pi, 3)
print(f"Trial {trial}: theta = {theta_random}")
verify_gradients_at_point(theta_random, demo_cost)运行结果:
Trial 0: theta = [3.44829694 4.49366732 3.78727399]
Epsilon=1.0e-05 vs 1.0e-06: max diff = 1.50e-10
Consistent: True
Trial 1: theta = [3.42360201 2.66190161 4.0582724 ]
Epsilon=1.0e-05 vs 1.0e-06: max diff = 1.22e-10
Consistent: True
Trial 2: theta = [2.74944154 5.60317502 6.0548717 ]
Epsilon=1.0e-05 vs 1.0e-06: max diff = 7.77e-11
Consistent: True
Trial 3: theta = [2.40923412 4.97455513 3.32314479]
Epsilon=1.0e-05 vs 1.0e-06: max diff = 8.88e-11
Consistent: True
Trial 4: theta = [3.56912924 5.8156952 0.44633272]
Epsilon=1.0e-05 vs 1.0e-06: max diff = 2.33e-10
Consistent: TrueCommon Debugging Checklist
When gradients appear incorrect, check the following:
graph TD
A["Gradients seem wrong"] --> B{"Parameter indexing<br>correct?"}
B -->|No| C["Fix param index<br>mapping"]
B -->|Yes| D{"Hamiltonian<br>matches circuit?"}
D -->|No| E["Verify PauliOperator<br>qubit ordering"]
D -->|Yes| F{"Finite-diff<br>epsilon appropriate?"}
F -->|No| G["Try epsilon in<br>[1e-4, 1e-7]"]
F -->|Yes| H{"Circuit depth<br>causes numerical error?"}
H -->|Yes| I["Reduce depth or<br>use higher precision"]
H -->|No| J["Check for barren<br>plateau (all grads ~0)"]
style A fill:#f44336,color:#fff
style C fill:#4CAF50,color:#fff
style E fill:#4CAF50,color:#fff
style G fill:#4CAF50,color:#fff
style I fill:#4CAF50,color:#fff
style J fill:#FF9800,color:#fffDetecting Barren Plateaus
A barren plateau occurs when gradient magnitudes vanish exponentially with system size, making optimization impossible. Detect it by monitoring gradient statistics:
def check_barren_plateau(cost_fn, n_params, n_samples=100):
"""
Estimate gradient variance across random parameter initializations.
Exponentially small variance indicates a barren plateau.
"""
grad_samples = np.zeros((n_samples, n_params))
for s in range(n_samples):
theta_rand = np.random.uniform(0, 2 * np.pi, n_params)
grads = numerical_gradient(cost_fn, theta_rand)
grad_samples[s] = grads
# Compute variance of each gradient component
grad_var = np.var(grad_samples, axis=0)
mean_var = np.mean(grad_var)
print(f"Mean gradient variance: {mean_var:.6e}")
print(f"Per-parameter variances: {grad_var}")
# Heuristic: if variance is below 1e-8, barren plateau is likely
if mean_var < 1e-8:
print("WARNING: Possible barren plateau detected!")
print(" Consider: reducing circuit depth, using local cost functions,")
print(" or initializing with identity-block structure.")
else:
print("Gradient variance appears healthy.")
return mean_var
# Run the check
check_barren_plateau(demo_cost, n_params=3)运行结果:
Mean gradient variance: 1.728972e-01
Per-parameter variances: [0.00993495 0.00960763 0.49914899]
Gradient variance appears healthy.Logging Gradient Norms During Optimization
Monitoring gradient norms during optimization helps identify convergence issues:
class GradientMonitor:
"""Track gradient norms across optimization iterations."""
def __init__(self):
self.grad_norms = []
self.energies = []
def record(self, energy, gradients):
"""Record energy and gradient norm for this iteration."""
self.energies.append(energy)
norm = np.linalg.norm(gradients)
self.grad_norms.append(norm)
def report(self):
"""Print a summary of gradient behavior."""
norms = np.array(self.grad_norms)
print(f"Total iterations: {len(norms)}")
print(f"Gradient norm range: [{np.min(norms):.6e}, {np.max(norms):.6e}]")
print(f"Final gradient norm: {norms[-1]:.6e}")
# Check if gradient is decreasing
if len(norms) > 10:
early = np.mean(norms[:10])
late = np.mean(norms[-10:])
print(f"Early avg norm: {early:.6e}, Late avg norm: {late:.6e}")
if late < early:
print("Gradient norm is decreasing (good convergence).")
else:
print("WARNING: Gradient norm not decreasing!")
# Usage during optimization
monitor = GradientMonitor()
def monitored_cost(theta):
energy = demo_cost(theta)
grads = numerical_gradient(demo_cost, theta)
monitor.record(energy, grads)
return energy
# Run with monitoring
result = minimize(monitored_cost, np.array([0.5, 1.0, 1.5]),
method='COBYLA', options={'maxiter': 50})
monitor.report()运行结果:
Total iterations: 50
Gradient norm range: [6.123176e-03, 1.017611e+00]
Final gradient norm: 6.123176e-03
Early avg norm: 5.582015e-01, Late avg norm: 1.120678e-02
Gradient norm is decreasing (good convergence).Performance Optimization
This section provides practical tips for improving the performance of variational quantum circuit computations in pyqpanda3.
Choose the Right Differentiation Method
| Method | Cost per gradient | Precision | Backend requirement |
|---|---|---|---|
| Adjoint | Machine precision | Statevector | |
| Parameter shift | Machine precision | Any backend | |
| Finite difference | Depends on | Any backend |
Recommendation: Always use adjoint differentiation when working with a statevector simulator. It computes all
# Preferred: adjoint differentiation (single pass)
diff_method = vqcircuit.DiffMethod.ADJOINT_DIFFReduce Circuit Depth
Shallower circuits lead to faster simulation and smaller numerical errors:
# Instead of deep hardware-efficient ansatz (many layers)
# Use problem-inspired ansatz with minimal depth
# BAD: Generic deep circuit
def deep_ansatz(n_qubits, depth=20):
prog = core.QProg()
for _ in range(depth):
for q in range(n_qubits):
prog << core.RY(q, 0.0) # placeholder
for q in range(n_qubits - 1):
prog << core.CNOT(q, q + 1)
return prog
# GOOD: Problem-tailored shallow circuit (e.g., for H2)
def shallow_h2_ansatz(theta):
"""Minimal 2-qubit UCCSD-inspired ansatz for H2."""
prog = core.QProg()
prog << core.X(1) # Initial Hartree-Fock state
prog << core.RY(0, theta[0])
prog << core.CNOT(0, 1)
prog << core.RY(0, theta[1])
return progExploit Hamiltonian Symmetry
Group commuting Pauli terms to reduce the number of circuit evaluations:
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
# When measuring a Hamiltonian with many Pauli terms,
# group terms that can be measured simultaneously (same Pauli basis)
# For example, Z0 Z1 and Z0 can both be measured in the Z basis
h_grouped = {
"Z_basis": {"Z0 Z1": -0.01128010, "Z0": 0.39793742, "Z1": -0.39793742},
"X_basis": {"X0 X1": 0.18093119},
"identity": {"": -1.0523792},
}
def efficient_cost(theta, prog_fn, grouped_h):
"""Compute energy using grouped Pauli measurements."""
prog = prog_fn(theta)
total = 0.0
for basis, terms in grouped_h.items():
if basis == "identity":
for pauli_str, coeff in terms.items():
total += coeff
continue
# Build a combined PauliOperator for this basis group
pauli_dict = {k: v for k, v in terms.items()}
op = PauliOperator(pauli_dict)
exp_val = core.expval_pauli_operator(prog, op)
total += exp_val
return totalOptimizer Selection Guide
graph TD
A["Choose optimizer"] --> B{"Gradient-based<br>or gradient-free?"}
B -->|"Gradient available"| C{"Noisy simulator<br>or ideal?"}
B -->|"Gradient-free"| D["COBYLA / Nelder-Mead"]
C -->|"Ideal (statevector)"| E["L-BFGS-B<br>(fast convergence)"]
C -->|"Noisy"| F["Adam / SPSA"]
D --> G["Good for few params,<br>robust to noise"]
E --> H["Fastest for smooth<br>landscapes"]
F --> I["Handles shot noise<br>and hardware errors"]
style A fill:#4CAF50,color:#fff
style E fill:#2196F3,color:#fff
style F fill:#FF9800,color:#fff
style D fill:#FF9800,color:#ffffrom pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
from scipy.optimize import minimize
import numpy as np
# Define a simple demo cost function (self-contained for this example)
def demo_cost_fn(theta):
prog = core.QProg()
prog << core.RY(0, theta[0]) << core.RY(1, theta[1])
prog << core.CNOT(0, 1)
return core.expval_pauli_operator(prog, PauliOperator({"Z0 Z1": 1.0}))
def demo_grad_fn(theta):
shift = np.pi / 2
grads = np.zeros(2)
for i in range(2):
tp = theta.copy(); tp[i] += shift
tm = theta.copy(); tm[i] -= shift
grads[i] = 0.5 * (demo_cost_fn(tp) - demo_cost_fn(tm))
return grads
theta0 = np.random.uniform(0, 2 * np.pi, 2)
# Gradient-free (robust, easy to use)
result_cobyla = minimize(demo_cost_fn, theta0, method='COBYLA',
options={'maxiter': 200})
# Gradient-based (faster convergence when gradients are accurate)
result_lbfgsb = minimize(demo_cost_fn, theta0, method='L-BFGS-B',
jac=demo_grad_fn,
options={'maxiter': 200})Parameter Initialization Strategies
Good initialization can drastically reduce the number of optimization iterations:
import numpy as np
# Guard against undefined n_params
try:
n_params
except NameError:
n_params = 12
# Guard against undefined p (QAOA layers)
try:
p
except NameError:
p = 2
# Strategy 1: Random uniform
theta_rand = np.random.uniform(0, 2 * np.pi, n_params)
# Strategy 2: Small random perturbation around zero
theta_small = np.random.normal(0, 0.1, n_params)
# Strategy 3: Identity-block initialization (avoids barren plateaus)
# Initialize so that U(theta_0) is close to identity
theta_identity = np.zeros(n_params)
# Strategy 4: Structured initialization based on problem symmetry
# For QAOA, initialize gamma near 0.5 and beta near 0.25
theta_qaoa = np.concatenate([
np.full(p, 0.5), # gamma parameters
np.full(p, 0.25), # beta parameters
])Caching Intermediate Results
When the cost function is called repeatedly with similar parameters, caching can save computation time:
from functools import lru_cache
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
# Cache the expectation value for Pauli terms (when circuit is deterministic)
# Note: only works when parameters are hashable (tuples, not arrays)
@lru_cache(maxsize=1024)
def cached_expectation(theta_tuple, pauli_str):
"""Cache expectation values keyed by parameter values and Pauli string."""
theta = np.array(theta_tuple)
prog = build_demo_circuit(theta)
op = PauliOperator({pauli_str: 1.0})
return core.expval_pauli_operator(prog, op)
# Convert theta to a hashable tuple before calling
theta_tuple = tuple(theta_test)
exp_zz = cached_expectation(theta_tuple, "Z0 Z1")Complete Example: VQE for H₂
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
# Step 1: Define the H₂ Hamiltonian (simplified)
# H = c0*I + c1*Z0 + c2*Z1 + c3*Z0*Z1 + c4*X0*X1 + c5*Y0*Y1
# Coefficients for bond length 0.735 Å
h2_coeffs = {
"": -1.0523792,
"Z0": 0.39793742,
"Z1": -0.39793742,
"Z0 Z1": -0.01128010,
"X0 X1": 0.18093119,
}
# Step 2: Define ansatz circuit (hardware-efficient)
def build_ansatz(params_array):
"""Build a parameterized ansatz for H₂."""
prog = core.QProg()
# Parameterized single-qubit rotations
prog << core.RY(0, params_array[0])
prog << core.RY(1, params_array[1])
# Entangling layer
prog << core.CNOT(0, 1)
# Second rotation layer
prog << core.RY(0, params_array[2])
prog << core.RY(1, params_array[3])
# Entangling layer
prog << core.CNOT(0, 1)
return prog
# Step 3: Define cost function
def cost_function(params_array):
"""Compute expectation value of H₂ Hamiltonian."""
prog = build_ansatz(params_array)
total = 0.0
for pauli_str, coeff in h2_coeffs.items():
if pauli_str == "":
total += coeff
continue
# Create Pauli operator for this term
op = PauliOperator({pauli_str: 1.0})
exp_val = core.expval_pauli_operator(prog, op, shots=1000)
total += coeff * exp_val
return total
# Step 4: Optimize using scipy
from scipy.optimize import minimize
# Initial parameters
theta0 = np.random.uniform(0, 2 * np.pi, 4)
result = minimize(cost_function, theta0, method='COBYLA',
options={'maxiter': 200, 'rhobeg': 0.5})
print(f"Optimal energy: {result.fun:.6f}")
print(f"Optimal parameters: {result.x}")
print(f"Expected ground state energy: -1.137275")运行结果:
Optimal energy: -1.857281
Optimal parameters: [1.16347281 4.71245821 4.52859848 4.71240423]
Expected ground state energy: -1.137275Interpreting VQE Results
The output of the VQE optimization provides several pieces of information that require careful interpretation.
Energy Accuracy
The exact ground state energy of
# Compare VQE result against the exact value
exact_energy = -1.137275
vqe_energy = result.fun
error = abs(vqe_energy - exact_energy)
print(f"VQE energy: {vqe_energy:.6f} Hartree")
print(f"Exact energy: {exact_energy:.6f} Hartree")
print(f"Absolute error: {error:.6f} Hartree")
print(f"Chemical accuracy threshold: 0.0016 Hartree (1 kcal/mol)")
if error < 0.0016:
print("Result is within chemical accuracy!")
else:
print("Result exceeds chemical accuracy; try more iterations or a better ansatz.")运行结果:
VQE energy: -1.857281 Hartree
Exact energy: -1.137275 Hartree
Absolute error: 0.720006 Hartree
Chemical accuracy threshold: 0.0016 Hartree (1 kcal/mol)
Result exceeds chemical accuracy; try more iterations or a better ansatz.Optimal Parameter Analysis
The optimized parameters reveal the structure of the ground state circuit:
# Inspect optimal parameters
optimal_theta = result.x
print("Optimal parameters (radians):")
for i, t in enumerate(optimal_theta):
print(f" theta[{i}] = {t:.6f} ({np.degrees(t):.2f} degrees)")
# Check if any parameters are near 0 or pi (indicates gate is effectively identity or Pauli)
for i, t in enumerate(optimal_theta):
t_mod = t % (2 * np.pi)
if t_mod < 0.1 or abs(t_mod - np.pi) < 0.1:
print(f" theta[{i}] is near a critical value (0 or pi)")运行结果:
Optimal parameters (radians):
theta[0] = 2.897387 (166.01 degrees)
theta[1] = 6.772629 (388.04 degrees)
theta[2] = 6.039491 (346.04 degrees)
theta[3] = 3.549784 (203.39 degrees)
theta[4] = 0.007031 (0.40 degrees)
theta[5] = 2.057295 (117.87 degrees)
theta[6] = 0.005938 (0.34 degrees)
theta[7] = 5.392649 (308.98 degrees)
theta[8] = 3.031328 (173.68 degrees)
theta[9] = 4.779705 (273.86 degrees)
theta[10] = 3.142273 (180.04 degrees)
theta[11] = 8.268002 (473.72 degrees)
theta[4] is near a critical value (0 or pi)
theta[6] is near a critical value (0 or pi)
theta[10] is near a critical value (0 or pi)State Composition
Reconstruct the optimal quantum state and examine which computational basis states contribute:
# Build circuit at optimal parameters
optimal_prog = build_ansatz(optimal_theta)
# Obtain the state vector
machine = core.CPUQVM()
machine.run(optimal_prog, shots=1000)
state = machine.result().get_state_vector()
# Print probability distribution over basis states
print("\nBasis state probabilities:")
for i, amp in enumerate(state):
prob = abs(amp) ** 2
if prob > 0.001:
label = format(i, '02b') # 2-bit binary string
# Map qubit ordering (bit 0 = rightmost)
print(f" |{label}> : amplitude = {amp:+.6f}, probability = {prob:.6f}")
# For H2, the ground state is approximately:
# |psi> ~ 0.9939|00> - 0.1102|11>
# This corresponds to the Hartree-Fock state with small double-excitation运行结果:
Basis state probabilities:
|01> : amplitude = -0.993761+0.000000j, probability = 0.987560
|10> : amplitude = +0.111534+0.000000j, probability = 0.012440Convergence Behavior
Analyze the optimizer's convergence trajectory:
# Track energy at each function evaluation
energy_history = []
def tracked_cost(params_array):
energy = cost_function(params_array)
energy_history.append(energy)
return energy
# Re-run optimization with tracking
theta0_retry = np.random.uniform(0, 2 * np.pi, 4)
tracked_result = minimize(tracked_cost, theta0_retry, method='COBYLA',
options={'maxiter': 200, 'rhobeg': 0.5})
# Report convergence statistics
print(f"Total function evaluations: {len(energy_history)}")
print(f"Initial energy: {energy_history[0]:.6f}")
print(f"Final energy: {energy_history[-1]:.6f}")
print(f"Energy improvement: {energy_history[0] - energy_history[-1]:.6f}")
# Check if energy stabilized (last 10 evaluations)
last_10 = energy_history[-10:]
energy_std = np.std(last_10)
print(f"Energy std (last 10 evals): {energy_std:.6e}")
if energy_std < 1e-4:
print("Energy has stabilized (good convergence).")
else:
print("Energy is still fluctuating; consider more iterations.")运行结果:
Total function evaluations: 99
Initial energy: -1.171267
Final energy: -1.857281
Energy improvement: 0.686014
Energy std (last 10 evals): 6.082143e-09
Energy has stabilized (good convergence).Scaling to Larger Molecules
The
| Molecule | Qubits | Parameters (UCCSD) | Pauli Terms |
|---|---|---|---|
| 2 | 4 | 5 | |
| LiH (STO-3G) | 4 | ~20 | ~100 |
| 6 | ~60 | ~200 | |
| 8 | ~150 | ~600 |
graph TD
A["H2 (2 qubits)"] --> B["LiH (4 qubits)"]
B --> C["BeH2 (6 qubits)"]
C --> D["H2O (8 qubits)"]
D --> E["Larger systems"]
A --- A1["5 Pauli terms<br>4 params"]
B --- B1["~100 terms<br>~20 params"]
C --- C1["~200 terms<br>~60 params"]
D --- D1["~600 terms<br>~150 params"]
style A fill:#4CAF50,color:#fff
style B fill:#8BC34A,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#f44336,color:#fff
style E fill:#9C27B0,color:#fffVariational Circuit Construction Patterns
Hardware-Efficient Ansatz
def hardware_efficient_ansatz(n_qubits, depth, params):
"""Build a hardware-efficient ansatz."""
prog = core.QProg()
idx = 0
for d in range(depth):
# Single-qubit rotation layer
for q in range(n_qubits):
prog << core.RY(q, params[idx])
idx += 1
prog << core.RZ(q, params[idx])
idx += 1
# Entangling layer (linear connectivity)
for q in range(n_qubits - 1):
prog << core.CNOT(q, q + 1)
return progQAOA Ansatz
def qaoa_ansatz(n_qubits, p, params, cost_hamiltonian):
"""Build a QAOA ansatz with p layers."""
gamma = params[:p] # Cost parameters
beta = params[p:2*p] # Mixer parameters
prog = core.QProg()
# Initial superposition
for q in range(n_qubits):
prog << core.H(q)
for layer in range(p):
# Cost layer (apply e^{-iγH_C})
# For Ising-type Hamiltonian, use RZZ gates
prog << core.RZZ(0, 1, 2 * gamma[layer])
# Add more problem-specific gates
# Mixer layer (apply e^{-iβH_M})
for q in range(n_qubits):
prog << core.RX(q, 2 * beta[layer])
return progDiffMethod Reference
ADJOINT_DIFF
The adjoint differentiation method:
- Reference: Jones and Gacon (2020)
- Complexity:
per gradient computation (vs circuit evaluations for parameter shift) - Precision: Machine precision (no statistical noise)
- Requirement: Statevector simulation backend
Gradient Result API Summary
ResGradients
| Method | Return | Description |
|---|---|---|
.at(idx) | float | Gradient for parameter at flat index idx |
.at([idx]) | float | Gradient for parameter at flat index idx |
.gradients() | list[float] | All gradient values |
.__len__() | int | Number of gradients |
.__str__() | str | String representation |
ResNGradients
| Method | Return | Description |
|---|---|---|
.at(idx) | ResGradients | Gradients for idx-th parameter set |
.data() | list[list[float]] | All gradient data |
.__len__() | int | Number of parameter sets (N) |
.__str__() | str | String representation |
ResGradientsAndExpectation
| Method | Return | Description |
|---|---|---|
.expectation_val() | float | Expectation value |
.gradients() | list[float] | All gradient values |
.at([idx]) | float | Gradient for specific parameter |
.data() | tuple[float, list[float]] | (expectation, [gradients]) |
.__str__() | str | String representation |
QAOA Complete Example: MaxCut
The Quantum Approximate Optimization Algorithm (QAOA) is a variational algorithm designed for combinatorial optimization problems. This section demonstrates QAOA applied to the MaxCut problem on a small graph.
Problem Definition
Given an undirected graph
This can be encoded as minimizing a cost Hamiltonian:
The negative sign ensures that minimizing
Example Graph
We use a 4-vertex ring graph with edges
graph LR
0 --- 1
1 --- 2
2 --- 3
3 --- 0
style 0 fill:#2196F3,color:#fff
style 1 fill:#f44336,color:#fff
style 2 fill:#2196F3,color:#fff
style 3 fill:#f44336,color:#fffThe optimal MaxCut value for this graph is 4 (all edges can be cut by partitioning {0,2} vs {1,3}).
QAOA Circuit Structure
The QAOA circuit with
where
graph TD
A["Hadamard layer<br>|+>^n"] --> B["Cost unitary<br>e^{-i*gamma*H_C}"]
B --> C["Mixer unitary<br>e^{-i*beta*H_M}"]
C --> D{More layers?}
D -->|Yes| B
D -->|No| E["Measure in<br>computational basis"]
style A fill:#4CAF50,color:#fff
style E fill:#2196F3,color:#fffStep 1: Define the Problem Graph
from pyqpanda3 import core, vqcircuit
from pyqpanda3.hamiltonian import PauliOperator
# Define the 4-vertex ring graph
edges = [(0, 1), (1, 2), (2, 3), (3, 0)]
n_qubits = 4
n_edges = len(edges)
print(f"Graph: {n_qubits} vertices, {n_edges} edges")
print(f"Edges: {edges}")
print(f"Optimal MaxCut value: {n_edges}")运行结果:
Graph: 4 vertices, 4 edges
Edges: [(0, 1), (1, 2), (2, 3), (3, 0)]
Optimal MaxCut value: 4Step 2: Construct the Cost Hamiltonian
The cost Hamiltonian encodes the MaxCut objective. Each edge contributes a
# Build the cost Hamiltonian from graph edges
# H_C = sum_{(i,j) in E} Z_i Z_j (we minimize this)
cost_terms = {}
for i, j in edges:
cost_terms[f"Z{i} Z{j}"] = 1.0
cost_hamiltonian = PauliOperator(cost_terms)
print(f"Cost Hamiltonian terms: {cost_terms}")运行结果:
Cost Hamiltonian terms: {'Z0 Z1': 1.0, 'Z1 Z2': 1.0, 'Z2 Z3': 1.0, 'Z3 Z0': 1.0}Step 3: Build the QAOA Ansatz
def build_qaoa_circuit(n_qubits, edges, gamma, beta):
"""
Build a QAOA circuit with parameters gamma (cost) and beta (mixer).
Args:
n_qubits: Number of qubits (vertices in the graph).
edges: List of (i, j) tuples defining the graph edges.
gamma: List of cost parameters, one per QAOA layer.
beta: List of mixer parameters, one per QAOA layer.
Returns:
A QProg containing the QAOA circuit.
"""
p = len(gamma) # Number of QAOA layers
assert len(beta) == p, "gamma and beta must have the same length"
prog = core.QProg()
# Step A: Prepare uniform superposition
for q in range(n_qubits):
prog << core.H(q)
# Step B: Apply p alternating layers
for layer in range(p):
# Cost unitary: e^{-i * gamma * H_C}
# For each edge (i, j), apply RZZ(i, j, 2 * gamma)
for i, j in edges:
prog << core.RZZ(i, j, 2 * gamma[layer])
# Mixer unitary: e^{-i * beta * H_M}
# For each qubit, apply RX(q, 2 * beta)
for q in range(n_qubits):
prog << core.RX(q, 2 * beta[layer])
return prog
# Test with a single layer
p = 1
gamma_init = [0.5]
beta_init = [0.25]
test_circuit = build_qaoa_circuit(n_qubits, edges, gamma_init, beta_init)
print("QAOA circuit built successfully")运行结果:
QAOA circuit built successfullyStep 4: Define the Cost Function
def qaoa_cost(params, n_qubits, edges, p):
"""
Compute the QAOA cost function (expectation of H_C).
Args:
params: Flat array of [gamma_0, ..., gamma_{p-1}, beta_0, ..., beta_{p-1}].
n_qubits: Number of qubits.
edges: Graph edges.
p: Number of QAOA layers.
Returns:
Expectation value of the cost Hamiltonian.
"""
gamma = params[:p].tolist()
beta = params[p:2*p].tolist()
prog = build_qaoa_circuit(n_qubits, edges, gamma, beta)
# Compute expectation of cost Hamiltonian
expectation = core.expval_pauli_operator(prog, cost_hamiltonian)
return expectation
# Evaluate at initial parameters
params_init = np.array(gamma_init + beta_init)
cost_0 = qaoa_cost(params_init, n_qubits, edges, p)
print(f"Initial cost: {cost_0:.6f}")运行结果:
Initial cost: 1.530295Step 5: Optimize the QAOA Parameters
import numpy as np
from scipy.optimize import minimize
# Run optimization with multiple random restarts
best_cost = float('inf')
best_params = None
n_restarts = 10
for restart in range(n_restarts):
# Random initial parameters
theta0 = np.random.uniform(0, np.pi, 2 * p)
# Minimize using COBYLA
result = minimize(
qaoa_cost,
theta0,
args=(n_qubits, edges, p),
method='COBYLA',
options={'maxiter': 300, 'rhobeg': 0.5}
)
if result.fun < best_cost:
best_cost = result.fun
best_params = result.x.copy()
print(f" Restart {restart}: cost = {result.fun:.6f}, "
f"converged = {result.success}")
print(f"\nBest cost found: {best_cost:.6f}")
print(f"Best gamma: {best_params[:p]}")
print(f"Best beta: {best_params[p:]}")运行结果:
Restart 0: cost = -2.000000, converged = True
Restart 1: cost = -2.000000, converged = True
Restart 2: cost = -2.000000, converged = True
Restart 3: cost = -2.000000, converged = True
Restart 4: cost = -2.000000, converged = True
Restart 5: cost = -2.000000, converged = True
Restart 6: cost = -2.000000, converged = True
Restart 7: cost = -2.000000, converged = True
Restart 8: cost = -2.000000, converged = True
Restart 9: cost = -2.000000, converged = True
Best cost found: -2.000000
Best gamma: [1.96347055]
Best beta: [2.74889785]Step 6: Sample the Optimal Solution
# Build circuit at optimal parameters
optimal_gamma = best_params[:p].tolist()
optimal_beta = best_params[p:].tolist()
optimal_prog = build_qaoa_circuit(n_qubits, edges, optimal_gamma, optimal_beta)
# Sample from the circuit to get candidate solutions
n_shots = 1000
machine = core.CPUQVM()
machine.run(optimal_prog, shots=n_shots)
counts = machine.result().get_counts()
# Evaluate each measured bitstring's cut value
print("\nTop solutions by frequency:")
sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)
for bitstring, count in sorted_counts[:5]:
# Compute the cut value for this bitstring
cut_value = 0
for i, j in edges:
if bitstring[i] != bitstring[j]:
cut_value += 1
probability = count / n_shots
print(f" {bitstring}: cut={cut_value}, "
f"count={count}, probability={probability:.3f}")
# Find the best bitstring overall
best_cut = 0
best_bitstring = None
for bitstring, count in counts.items():
cut_value = 0
for i, j in edges:
if bitstring[i] != bitstring[j]:
cut_value += 1
if cut_value > best_cut:
best_cut = cut_value
best_bitstring = bitstring
print(f"\nBest solution found: {best_bitstring} with cut value {best_cut}")
print(f"Optimal cut value: {n_edges}")
print(f"Approximation ratio: {best_cut / n_edges:.4f}")运行结果:
Top solutions by frequency:
Best solution found: None with cut value 0
Optimal cut value: 4
Approximation ratio: 0.0000Step 7: Increase QAOA Depth
Higher
# Run QAOA with p=2 layers for better approximation
p = 2
best_cost_p2 = float('inf')
best_params_p2 = None
for restart in range(10):
theta0 = np.random.uniform(0, np.pi, 2 * p)
result = minimize(
qaoa_cost,
theta0,
args=(n_qubits, edges, p),
method='COBYLA',
options={'maxiter': 300, 'rhobeg': 0.5}
)
if result.fun < best_cost_p2:
best_cost_p2 = result.fun
best_params_p2 = result.x.copy()
print(f"\np=1 best cost: {best_cost:.6f}")
print(f"p=2 best cost: {best_cost_p2:.6f}")
print(f"Expected optimal: {-n_edges:.1f} (all edges cut)")运行结果:
p=1 best cost: -2.000000
p=2 best cost: -4.000000
Expected optimal: -4.0 (all edges cut)QAOA Approximation Ratio
The approximation ratio measures how close the QAOA result is to the optimal solution:
# Compute approximation ratios
optimal_cut = n_edges # For the 4-vertex ring, optimal cut = 4
# The cost Hamiltonian is sum of Z_i Z_j
# When all edges are cut, each Z_i Z_j = -1, so cost = -n_edges
# Approximation ratio: |cost| / n_edges
alpha_p1 = abs(best_cost) / optimal_cut
alpha_p2 = abs(best_cost_p2) / optimal_cut
print(f"Approximation ratio (p=1): {alpha_p1:.4f}")
print(f"Approximation ratio (p=2): {alpha_p2:.4f}")
# Theoretical guarantee: QAOA with p=1 achieves alpha >= 0.6924
# for 3-regular graphs (Farhi et al. 2014)
print(f"\nTheoretical p=1 lower bound (3-regular): 0.6924")运行结果:
Approximation ratio (p=1): 0.5000
Approximation ratio (p=2): 1.0000
Theoretical p=1 lower bound (3-regular): 0.6924Extending to Larger Graphs
The same framework applies to larger graphs. Here is a helper for arbitrary graph definitions:
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
def solve_maxcut_qaoa(edges, n_qubits, p=1, n_restarts=20):
"""
Solve MaxCut using QAOA for an arbitrary graph.
Args:
edges: List of (i, j) tuples defining the graph.
n_qubits: Number of vertices.
p: QAOA depth parameter.
n_restarts: Number of random restarts for optimization.
Returns:
Dictionary with optimization results.
"""
# Build cost Hamiltonian
cost_terms = {f"Z{i} Z{j}": 1.0 for i, j in edges}
cost_H = PauliOperator(cost_terms)
best_cost = float('inf')
best_params = None
for restart in range(n_restarts):
theta0 = np.random.uniform(0, np.pi, 2 * p)
def cost_fn(params):
gamma = params[:p].tolist()
beta = params[p:2*p].tolist()
prog = build_qaoa_circuit(n_qubits, edges, gamma, beta)
return core.expval_pauli_operator(prog, cost_H)
result = minimize(cost_fn, theta0, method='COBYLA',
options={'maxiter': 300})
if result.fun < best_cost:
best_cost = result.fun
best_params = result.x.copy()
# Sample the optimal circuit
gamma = best_params[:p].tolist()
beta = best_params[p:].tolist()
optimal_prog = build_qaoa_circuit(n_qubits, edges, gamma, beta)
machine = core.CPUQVM()
machine.run(optimal_prog, shots=2000)
counts = machine.result().get_counts()
# Find best bitstring
best_cut = 0
for bitstring in counts:
cut = sum(1 for i, j in edges if bitstring[i] != bitstring[j])
best_cut = max(best_cut, cut)
return {
'best_cost': best_cost,
'best_params': best_params,
'best_cut': best_cut,
'optimal_cut': len(edges),
'approximation_ratio': best_cut / len(edges),
'counts': counts,
}
# Example: solve for a 5-vertex path graph
path_edges = [(0, 1), (1, 2), (2, 3), (3, 4)]
result_path = solve_maxcut_qaoa(path_edges, n_qubits=5, p=2)
print(f"Path graph result: cut={result_path['best_cut']}/{result_path['optimal_cut']}, "
f"ratio={result_path['approximation_ratio']:.4f}")运行结果:
Path graph result: cut=0/4, ratio=0.0000Next Steps
- Hamiltonian & Pauli Operators - Define observables for VQE cost functions
- Simulation - Run variational circuits on simulators
- Quantum Information - Analyze variational state properties