Skip to content

Hamiltonian & Pauli Operators

This tutorial introduces the Hamiltonian and Pauli operator framework in pyqpanda3. You will learn how to construct Pauli operators, compose Hamiltonians, and compute expectation values -- the essential building blocks for variational quantum algorithms such as VQE and QAOA.

Prerequisites: Simulation -- you should be familiar with creating quantum programs and running them on simulators.


Table of Contents


1. What Is a Hamiltonian?

In quantum mechanics, a Hamiltonian H is the operator corresponding to the total energy of a system. It governs how a quantum state evolves in time via the Schrodinger equation:

it|ψ(t)=H|ψ(t)

The Energy Eigenvalue Problem

The eigenstates of the Hamiltonian satisfy:

H|ψn=En|ψn

where En are the energy eigenvalues. The ground state is the eigenstate with the lowest energy E0.

The Variational Principle

The variational principle provides a powerful tool for approximating ground-state energies. For any trial state |ψ:

E0ψ|H|ψ

This inequality means that the expectation value of H with respect to any state |ψ is always greater than or equal to the true ground-state energy. This principle is the foundation of the Variational Quantum Eigensolver (VQE) algorithm:

  1. Prepare a parameterized trial state |ψ(θ).
  2. Measure the expectation value H=ψ(θ)|H|ψ(θ).
  3. Use a classical optimizer to minimize H over θ.
  4. The minimum gives an upper bound on E0.

Why Pauli Operators?

Quantum hardware can only directly measure in the computational (Z) basis. To measure a general Hamiltonian, we decompose it into a sum of Pauli operators:

H=iciPi,Pi{I,X,Y,Z}n

where each Pi is a tensor product of single-qubit Pauli matrices (including the identity), and ci is a real or complex coefficient. By the linearity of expectation:

H=iciPi

Each term Pi can be measured separately (or grouped with other commuting terms).


2. Pauli Matrices Refresher

The four single-qubit Pauli matrices are:

σI=(1001),σX=(0110)σY=(0ii0),σZ=(1001)

Key properties:

PropertyExpression
Hermitianσi=σi
Unitaryσi2=I
Traceless (X, Y, Z)Tr(σi)=0
Anticommutationσiσj=σjσi for ij
Commutation[σi,σj]=2iϵijkσk

Multi-qubit Pauli operators are formed by tensor products. For example:

XZ=X0Z1=(0010000110000100)

The subscript indicates the qubit index the Pauli gate acts on.


3. The PauliOperator Class

The PauliOperator class in pyqpanda3.hamiltonian represents a linear combination of multi-qubit Pauli terms. It is the primary way to define Hamiltonians and observables in pyqpanda3.

python
from pyqpanda3.hamiltonian import PauliOperator

3.1 Construction

PauliOperator provides several constructors to accommodate different input formats. The following diagram summarizes the available construction paths:

Default and Copy Constructors

python
# Create an empty PauliOperator
op_empty = PauliOperator()

# Copy an existing operator
op_copy = PauliOperator(op_existing)

From a String

A single Pauli term without an explicit coefficient (defaults to coefficient 1.0):

python
# Single-qubit Pauli-X on qubit 0
op_x = PauliOperator("X0")

# Multi-qubit term: X on qubit 0, Z on qubit 1
op_xz = PauliOperator("X0 Z1")

# Three-qubit term
op_xyz = PauliOperator("X0 Y1 Z2")

The string format uses the pattern <Pauli><qubit_index> separated by spaces. Valid Pauli characters are X, Y, Z, and I.

From a Dictionary

The most common construction method -- a dictionary mapping Pauli strings to coefficients:

python
# Multiple terms with real coefficients
op = PauliOperator({
    "X0 Z1": 1.0,
    "Z0 Z1": 0.5,
    "Y0 X1": -0.3
})

# Complex coefficients are also supported
import numpy as np
op_complex = PauliOperator({
    "X0 Z1": 1.0 + 0.5j,
    "Z0 Z1": -0.2 + 0.1j
})

This is the recommended way to construct a multi-term operator.

From Lists of Strings and Coefficients

Two parallel lists: one for Pauli strings and one for coefficients:

python
paulis = ["X0 Z1", "Y2 I1"]
coefs = [1.1, 2.1]

op = PauliOperator(paulis, coefs)

Both lists must have the same length.

From Lists with the AB Flag

When AB_is_A1_B0=True, the Pauli string is interpreted as a compact sequence without explicit qubit indices. The character positions map to qubits: position 0 maps to qubit 0, position 1 to qubit 1, and so on.

python
# "XZI" means: X on qubit 0, Z on qubit 1, I on qubit 2
# "IZX" means: I on qubit 0, Z on qubit 1, X on qubit 2
op = PauliOperator(["XZI", "IZX"], [1.1, 2.1], True)

When AB_is_A1_B0=False, the string positions are interpreted in reverse qubit order (position 0 maps to the highest-index qubit).

From a List of Tuples

Each tuple contains (pauli_string, qubit_indices, coefficient):

python
# ("XZ", [0, 4], 1.1) means X on qubit 0, Z on qubit 4, with coefficient 1.1
# ("YX", [1, 2], 2.1) means Y on qubit 1, X on qubit 2, with coefficient 2.1
op = PauliOperator([
    ("XZ", [0, 4], 1.1),
    ("YX", [1, 2], 2.1)
])

This format gives explicit control over which qubits each Pauli character acts on.

From a Numpy Matrix

A real-valued square matrix of dimension 2n×2n is automatically decomposed into its Pauli representation:

python
import numpy as np

# A 2x2 matrix (single qubit)
mat = np.array([[1.0, 0.0],
                [0.0, -1.0]])

op = PauliOperator(mat)
# This is equivalent to PauliOperator({"Z0": 1.0})

The matrix must have dimensions that are a power of 2. Internally, pyqpanda3 performs a Pauli decomposition of the input matrix.

3.2 Algebraic Operations

PauliOperator supports the standard arithmetic operators. All operations return new objects (immutability is preserved).

Addition and Subtraction

python
from pyqpanda3.hamiltonian import PauliOperator

op1 = PauliOperator({"X0 Z1": 1.0, "Z0 Z1": 0.5})
op2 = PauliOperator({"Y0 X1": 0.3})

# Addition of two operators
op_sum = op1 + op2
# Result: {"X0 Z1": 1.0, "Z0 Z1": 0.5, "Y0 X1": 0.3}

# Subtraction
op_diff = op1 - op2
# Result: {"X0 Z1": 1.0, "Z0 Z1": 0.5, "Y0 X1": -0.3}

# Adding a scalar (adds a constant term, equivalent to c * I)
op_shifted = op1 + 1.5

# Scalar subtraction
op_lowered = op1 - 0.5

When two terms share the same Pauli string, their coefficients are combined:

python
a = PauliOperator({"Z0": 1.0})
b = PauliOperator({"Z0": 2.5})
c = a + b
# Result: {"Z0": 3.5}

If the coefficients cancel out, the term is automatically removed:

python
a = PauliOperator({"X0": 1.0})
b = PauliOperator({"X0": 1.0})
c = a - b
# Result: empty operator (no terms)

Scalar Multiplication and Division

python
op = PauliOperator({"X0 Z1": 2.0, "Y0": 1.0})

# Multiply by a scalar
op_doubled = op * 2.0
# Result: {"X0 Z1": 4.0, "Y0": 2.0}

# Left-multiplication is also supported
op_tripled = 3.0 * op
# Result: {"X0 Z1": 6.0, "Y0": 3.0}

# Division
op_halved = op * 0.5
# Result: {"X0 Z1": 1.0, "Y0": 0.5}

Operator Multiplication

The * operator performs algebraic multiplication. For each pair of terms from the left and right operands, the Pauli strings are multiplied following the Pauli algebra rules:

XY=iZ,YZ=iX,ZX=iYXX=I,YY=I,ZZ=I
python
a = PauliOperator({"X0": 1.0})
b = PauliOperator({"Y0": 1.0})
c = a * b
# X * Y = iZ, so result: {"Z0": 1j}

The @ operator (matrix multiplication) is an alias for *:

python
d = a @ b
# Same result as a * b

Negation

python
op = PauliOperator({"X0 Z1": 1.5})
neg = -op
# Result: {"X0 Z1": -1.5}

In-Place Operations

While the binary operators return new objects, the in-place variants (+=, -=, *=) modify the left operand:

python
op = PauliOperator({"X0": 1.0})
op += PauliOperator({"Y0": 0.5})
# op is now {"X0": 1.0, "Y0": 0.5}

op *= 2.0
# op is now {"X0": 2.0, "Y0": 1.0}

3.3 Tensor Products

The tensor product (Kronecker product) combines two operators acting on separate sets of qubits. In pyqpanda3, use the .tensor() method:

python
a = PauliOperator({"X0": 1.0})   # Acts on qubit 0
b = PauliOperator({"Z0": 0.5})   # Acts on qubit 0

# Tensor product: a's qubits stay the same, b's qubits are shifted
result = a.tensor(b)
# Result: X on qubit 0, Z on qubit 1, coefficient 0.5
# Equivalent to PauliOperator({"X0 Z1": 0.5})

You can also tensor an operator with itself n times:

python
op = PauliOperator({"X0": 1.0})

# Tensor product of op with itself 3 times
# Result: X on qubit 0, X on qubit 1, X on qubit 2
result = op.tensor(3)

3.4 Inspecting and Converting

String Representations

python
op = PauliOperator({"X0 Z1": 1.0, "Y2": 0.5, "I0 Z1": 0.3})

# String without identity operators (default __str__)
print(op)
# Output includes only non-trivial Paulis, e.g. "X0 Z1" + 1.0, "Y2" + 0.5, ...

# String with identity operators included
print(op.str_with_I(True))
# Shows full Pauli string for each term including I

# String without identity (alternative method)
print(op.str_no_I())

The AB_is_A1_B0 parameter controls the ordering of qubit indices in the string representation.

Matrix Conversion

Convert a PauliOperator to its matrix representation:

python
op = PauliOperator({"Z0": 1.0})
mat = op.matrix()
print(mat)
# Output (numpy complex matrix):
# [[1+0j, 0+0j],
#  [0+0j, -1+0j]]

For multi-qubit operators, the matrix dimension is 2n+1×2n+1 where n is the highest qubit index:

python
op2 = PauliOperator({"X0 Z1": 1.0})
mat2 = op2.matrix()
# 4x4 matrix
print(mat2.shape)  # (4, 4)

Accessing Internal Data

Several methods expose the operator's internal data in different formats:

python
op = PauliOperator({"X0 Z1": 1.5, "Y2": -0.3})

# List of 3-tuples: (pauli_string, qubit_indices, complex_coefficient)
data_complex = op.data_3tuple_list_complex_coeff()
# [("XZ", [0, 1], 1.5+0j), ("Y", [2], -0.3+0j)]

# List of 3-tuples with float coefficients
data_float = op.data_3tuple_list_float_coeff()
# [("XZ", [0, 1], 1.5), ("Y", [2], -0.3)]

# Dictionary with string keys and float values
data_dict = op.data_dict_float_coeff()
# {"X0 Z1": 1.5, "Y2": -0.3}

# Dictionary with string keys and complex values
data_dict_c = op.data_dict_complex_coeff()
# {"X0 Z1": (1.5+0j), "Y2": (-0.3+0j)}

Getting the Qubit List

python
op = PauliOperator({"X0 I1 Z2": 1.0})
i_qubits, xyz_qubits = op.qubits()
# i_qubits: [1]          -- qubits only acted on by I
# xyz_qubits: [0, 2]     -- qubits acted on by X, Y, or Z

Maximum Qubit Index

python
op = PauliOperator({"X0 Z3": 1.0})
print(op.max_qbit_idx())  # 3

Terms Access

python
op = PauliOperator({"X0 Z1": 1.0, "Y2": 0.5})
terms = op.terms()
for term in terms:
    print(f"Coefficient: {term.coef()}")
    paulis = term.paulis()
    for pwq in paulis:
        print(f"  Qubit {pwq.qbit()}: {pwq.pauli_char()}")

Serialization

python
op = PauliOperator({"Z0": 0.5, "X1": -0.3})

# Serialize to string
s = op.data_dict_str()
# e.g., '"Z0": 0.5, "X1": -0.3'

# Deserialize from string
op_restored = PauliOperator.from_dict_str(s)

3.5 Commuting Groups

For efficient measurement, you can group commuting Pauli terms together:

python
op = PauliOperator({
    "X0 X1": 1.0,
    "Z0 Z1": 0.5,
    "Y0 Y1": -0.3,
    "Z0": 0.2
})

# Group by full commutation
groups = op.group_commuting(qubit_wise=False)

# Group by qubit-wise commutation (stricter grouping)
groups_qw = op.group_commuting(qubit_wise=True)

for i, group in enumerate(groups):
    print(f"Group {i}: {group}")

Qubit-wise commuting groups can be measured simultaneously using a single basis rotation per group, which is crucial for reducing the number of circuit executions in variational algorithms.


4. The Hamiltonian Class

The Hamiltonian class in pyqpanda3.hamiltonian is a thin wrapper around PauliOperator that represents a quantum Hamiltonian. It supports the same constructors and algebraic operations as PauliOperator but is used specifically in contexts where the operator represents a physical Hamiltonian.

python
from pyqpanda3.hamiltonian import Hamiltonian, PauliOperator

Construction

Hamiltonian supports the same construction patterns as PauliOperator:

python
# Default (empty)
H1 = Hamiltonian()

# From a dictionary of Pauli strings
H2 = Hamiltonian({"X0 Z1": 1.0, "Z0 Z1": 0.5})

# From a PauliOperator
pauli_op = PauliOperator({"X0 Z1": 1.0, "Z0 Z1": 0.5})
H3 = Hamiltonian(pauli_op)

# From a string (single Pauli on qubit 0)
H4 = Hamiltonian("Z")

# From lists
H5 = Hamiltonian(["X0 Z1", "Y2 I1"], [1.1, 2.1])

# From lists with AB flag
H6 = Hamiltonian(["XZI", "IZX"], [1.1, 2.1], True)

# From tuples
H7 = Hamiltonian([("XZ", [0, 4], 1.1), ("YX", [1, 2], 2.1)])

# From a numpy matrix
import numpy as np
H8 = Hamiltonian(np.array([[1, 0], [0, -1]]))

Algebraic Operations

The arithmetic operators work identically to PauliOperator:

python
ha = Hamiltonian({"X0 Z1": 1.0})
hb = Hamiltonian({"Y0 X1": 0.5})

# Addition
h_sum = ha + hb

# Subtraction
h_diff = ha - hb

# Scalar multiplication
h_scaled = ha * 2.0

# Operator multiplication
h_prod = ha * hb

# Tensor product
h_tensor = ha.tensor(hb)

# Negation
h_neg = -ha

Extracting the Underlying PauliOperator

python
H = Hamiltonian({"Z0": 1.0, "X0": 0.5})

# Get the underlying PauliOperator
pauli = H.pauli_operator()

# Convert to matrix
mat = H.matrix()

String Representation

python
H = Hamiltonian({"X0 Z1": 1.0, "Y2": 0.5})

print(H)              # Default string (without I)
print(H.str_with_I()) # Full string with identity operators
print(H.str_no_I())   # Compact string without I

When to Use Hamiltonian vs PauliOperator

  • Use PauliOperator when you need fine-grained access to individual terms, Pauli decomposition data, commuting group analysis, or when building observables for general purposes.
  • Use Hamiltonian when interfacing with functions that specifically expect a Hamiltonian type, such as expval_hamiltonian().

Both types can be used interchangeably in most expectation value functions, as pyqpanda3 provides separate APIs for each.


5. Expectation Value Computation

Computing the expectation value ψ|H|ψ is the central operation in variational quantum algorithms. pyqpanda3 provides two functions for this purpose.

5.1 Core API

Both functions are exported by pyqpanda3.core:

python
from pyqpanda3.core import expval_hamiltonian, expval_pauli_operator, QProg, CPUQVM
from pyqpanda3.hamiltonian import Hamiltonian, PauliOperator

expval_hamiltonian

python
expval_hamiltonian(
    prog,             # QProg -- the quantum program
    hamiltonian,      # Hamiltonian -- the observable
    shots=1,          # int -- number of measurement shots (1 = state vector)
    model=NoiseModel(),  # optional noise model
    used_threads=4,   # int -- number of parallel threads
    backend="CPU"     # str -- "CPU" or "GPU"
) -> float

expval_pauli_operator

python
expval_pauli_operator(
    prog,             # QProg -- the quantum program
    pauli_operator,   # PauliOperator -- the observable
    shots=1,          # int -- number of measurement shots (1 = state vector)
    model=NoiseModel(),  # optional noise model
    used_threads=4,   # int -- number of parallel threads
    backend="CPU"     # str -- "CPU" or "GPU"
) -> float

The following diagram illustrates the expectation value computation pipeline:

When shots=1, the function computes the exact expectation value using state vector simulation. When shots > 1, it performs measurement-based estimation with the specified number of shots.

5.2 Example: Bell State

Let us compute the expectation value of a Pauli operator with respect to a Bell state |Φ+=12(|00+|11):

python
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator

# Create a Bell state: |00> + |11>
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.BARRIER([0, 1])

# Define the observable: Z0 * Z1
# For |00> + |11>, the expectation of ZZ is 1.0
# because ZZ|00> = |00> and ZZ|11> = |11>
op_zz = PauliOperator({"Z0 Z1": 1.0})

# Compute exact expectation value (shots=1)
exp_val = core.expval_pauli_operator(prog, op_zz)
print(f"<ZZ> = {exp_val}")  # Expected: 1.0

# Compute with finite shots (statistical estimate)
exp_val_shots = core.expval_pauli_operator(prog, op_zz, shots=1000)
print(f"<ZZ> (1000 shots) = {exp_val_shots}")  # Close to 1.0

Multi-term Operator

python
# Define a multi-term operator
op = PauliOperator({
    "Z0 Z1": 0.5,   # ZZ interaction
    "X0 X1": 0.3,   # XX interaction
    "Y0 Y1": 0.2    # YY interaction
})

exp_val = core.expval_pauli_operator(prog, op)
print(f"<H> = {exp_val}")

The expectation value is computed as:

H=0.5Z0Z1+0.3X0X1+0.2Y0Y1

For the Bell state |Φ+:

Z0Z1=1,X0X1=1,Y0Y1=1

So the expected result is 0.5(1)+0.3(1)+0.2(1)=0.6.

Using the Hamiltonian API

python
from pyqpanda3.hamiltonian import Hamiltonian

H = Hamiltonian({"Z0 Z1": 0.5, "X0 X1": 0.3})

exp_val = core.expval_hamiltonian(prog, H)
print(f"<H> = {exp_val}")

5.3 Using Noise Models

You can include a noise model when computing expectation values to simulate realistic hardware conditions:

python
from pyqpanda3.core import NoiseModel, pauli_x_error, depolarizing_error, GateType

# Build a noise model
model = NoiseModel()
model.add_all_qubit_quantum_error(pauli_x_error(0.01), GateType.H)
model.add_all_qubit_quantum_error(depolarizing_error(0.02), GateType.CNOT)

# Create circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.BARRIER([0, 1])

# Define observable
op = PauliOperator({"Z0 Z1": 1.0})

# Compute noisy expectation value
exp_val_noisy = core.expval_pauli_operator(
    prog, op,
    shots=1000,
    model=model
)
print(f"<ZZ> (noisy) = {exp_val_noisy}")

With noise, the expectation value will deviate from the ideal value of 1.0 due to gate errors.


6. PauliTerm and PauliWithQbit Helper Classes

Internally, a PauliOperator is composed of PauliTerm objects, each of which contains PauliWithQbit entries. These classes provide a structured way to inspect individual terms.

PauliTerm

A PauliTerm represents a single term cP0P1 in the Pauli decomposition.

python
op = PauliOperator({"X0 Z1": 1.5, "Y2": -0.3})
terms = op.terms()

for term in terms:
    # Get the coefficient
    c = term.coef()
    print(f"Coefficient: {c}")

    # Get all Pauli-qubit pairs
    paulis = term.paulis()
    for pwq in paulis:
        print(f"  Qubit {pwq.qbit()}: Pauli {pwq.pauli_char()}")

    # Get qubit classification
    i_qubits, xyz_qubits = term.qubits()
    print(f"  I-only qubits: {i_qubits}")
    print(f"  XYZ qubits: {xyz_qubits}")

    # Export data in various formats
    data_3c = term.data_3tuple_complex_coeff()
    data_2c = term.data_2tuple_complex_coeff()
    data_3f = term.data_3tuple_float_coeff()
    data_2f = term.data_2tuple_float_coeff()

    # Convert to a quantum circuit
    circuit = term.to_qcircuit()

PauliWithQbit

A PauliWithQbit pairs a single Pauli operator with a specific qubit:

python
op = PauliOperator({"X0 Y1 Z2": 1.0})
terms = op.terms()

for term in terms:
    for pwq in term.paulis():
        qubit = pwq.qbit()          # Qubit index
        is_x = pwq.is_X()           # True if this is Pauli-X
        is_y = pwq.is_Y()           # True if this is Pauli-Y
        is_z = pwq.is_Z()           # True if this is Pauli-Z
        is_i = pwq.is_I()           # True if this is identity
        char = pwq.pauli_char()     # 'X', 'Y', 'Z', or 'I'
        gate = pwq.to_qgate()       # Corresponding QGate object

        print(f"Qubit {qubit}: {char}")

7. Practical Example: VQE for the H2 Molecule

This section demonstrates a complete workflow using the Variational Quantum Eigensolver (VQE) to estimate the ground-state energy of the H2 molecule.

Background

The electronic Hamiltonian of H2 at equilibrium bond length, after Jordan-Wigner transformation and freezing core orbitals, can be expressed as a 2-qubit Pauli operator:

HH2=c0I+c1Z0+c2Z1+c3Z0Z1+c4X0X1

For a bond distance of approximately 0.74 Angstroms, the coefficients are:

CoefficientValue
c0-1.0523792
c1-0.3979374
c2-0.3979374
c3-0.0112801
c40.1809313

The exact ground-state energy is approximately 1.137 Hartree.

Step 1: Define the Hamiltonian

python
from pyqpanda3.hamiltonian import PauliOperator

# H2 Hamiltonian coefficients (Hartree units)
h2_coefficients = {
    "": -1.0523792,       # Constant (identity) term
    "Z0": -0.3979374,     # Single-qubit Z on qubit 0
    "Z1": -0.3979374,     # Single-qubit Z on qubit 1
    "Z0 Z1": -0.0112801,  # ZZ interaction
    "X0 X1": 0.1809313    # XX interaction
}

# Note: the identity term is added as a scalar offset
h2_hamiltonian = PauliOperator({
    "Z0": -0.3979374,
    "Z1": -0.3979374,
    "Z0 Z1": -0.0112801,
    "X0 X1": 0.1809313
})

# Add the constant (identity) term
h2_hamiltonian += -1.0523792

print("H2 Hamiltonian:")
print(h2_hamiltonian)

Step 2: Build the Ansatz Circuit

We use a hardware-efficient ansatz with a single parameter:

python
from pyqpanda3 import core
import numpy as np

def build_ansatz(theta):
    """Build a parameterized ansatz circuit for H2.

    The ansatz prepares:
    |psi(theta)> = RY(theta)_0 |01>

    This creates a state that explores the X0X1 subspace
    relevant to the H2 Hamiltonian.
    """
    prog = core.QProg()
    # Prepare a reference state
    prog << core.X(1)
    # Apply entangling and rotation gates
    prog << core.RY(0, theta)
    prog << core.CNOT(0, 1)
    prog << core.BARRIER([0, 1])
    return prog

Step 3: Define the Objective Function

python
def objective(theta):
    """Compute <H2> for a given parameter theta."""
    prog = build_ansatz(theta)
    expectation = core.expval_pauli_operator(prog, h2_hamiltonian)
    return expectation

Step 4: Optimize

Using a simple grid search for illustration (in practice, you would use scipy.optimize.minimize):

python
# Grid search over theta
thetas = np.linspace(0, 2 * np.pi, 100)
energies = [objective(t) for t in thetas]

# Find minimum
min_idx = np.argmin(energies)
optimal_theta = thetas[min_idx]
min_energy = energies[min_idx]

print(f"Optimal theta: {optimal_theta:.4f}")
print(f"Minimum energy: {min_energy:.6f} Hartree")
print(f"Exact ground state: -1.137274 Hartree")
print(f"Error: {abs(min_energy - (-1.137274)):.6f} Hartree")

Step 5: Using scipy.optimize

For a more robust optimization:

python
from scipy.optimize import minimize

result = minimize(
    objective,
    x0=[0.0],           # Initial guess
    method='COBYLA',     # Gradient-free optimizer
    options={'maxiter': 200}
)

print(f"Optimization result:")
print(f"  Theta: {result.x[0]:.6f}")
print(f"  Energy: {result.fun:.6f} Hartree")
print(f"  Converged: {result.success}")

Complete Script

python
"""
VQE for H2 molecule using pyqpanda3
Estimates the ground-state energy of the H2 Hamiltonian.
"""
from pyqpanda3 import core
from pyqpanda3.hamiltonian import PauliOperator
import numpy as np
from scipy.optimize import minimize

# --- Define the H2 Hamiltonian ---
h2 = PauliOperator({
    "Z0": -0.3979374,
    "Z1": -0.3979374,
    "Z0 Z1": -0.0112801,
    "X0 X1": 0.1809313
})
h2 += -1.0523792  # Identity offset

# --- Ansatz ---
def build_ansatz(theta):
    prog = core.QProg()
    prog << core.X(1)
    prog << core.RY(0, theta)
    prog << core.CNOT(0, 1)
    prog << core.BARRIER([0, 1])
    return prog

# --- Objective ---
def objective(params):
    theta = params[0]
    prog = build_ansatz(theta)
    return core.expval_pauli_operator(prog, h2)

# --- Optimize ---
result = minimize(objective, x0=[0.0], method='COBYLA')
print(f"Ground state energy estimate: {result.fun:.6f} Hartree")
print(f"Optimal parameter: {result.x[0]:.6f}")

The output should be close to 1.137 Hartree, which is the exact ground-state energy of the H2 molecule at equilibrium.


8. Summary

This tutorial covered the essential components for working with Hamiltonians and Pauli operators in pyqpanda3:

ComponentDescription
PauliOperatorRepresents a linear combination of multi-qubit Pauli terms
HamiltonianA wrapper around PauliOperator for physical Hamiltonians
PauliTermA single term in a Pauli decomposition
PauliWithQbitA Pauli operator acting on a specific qubit
expval_pauli_operator()Computes ψ|P|ψ for a PauliOperator
expval_hamiltonian()Computes ψ|H|ψ for a Hamiltonian

Key Takeaways

  1. Construction flexibility: PauliOperator and Hamiltonian can be constructed from strings, dictionaries, lists, tuples, or numpy matrices.
  2. Algebraic operations: The standard Python operators (+, -, *, @) and .tensor() method enable expressive operator algebra.
  3. Expectation values: Use expval_pauli_operator() or expval_hamiltonian() with a QProg to compute expectation values, optionally with noise models and multiple shots.
  4. VQE workflow: Define the Hamiltonian as a Pauli decomposition, build a parameterized ansatz circuit, compute the expectation value, and optimize classically.

Next Steps

Released under the MIT License.