From 789fd55e83ae2af6c04f03265d00fe93f61e1712 Mon Sep 17 00:00:00 2001 From: debaobai Date: Sat, 6 Jun 2026 06:38:18 +0000 Subject: [PATCH] feat: add VQE (Variational Quantum Eigensolver) module - PauliHamiltonian: Hamiltonian as weighted sum of Pauli strings - hardware_efficient_ansatz & ry_linear_ansatz: parameterized circuits - measure_expectation: Pauli term expectation via probability readout - VQESolver: hybrid quantum-classical optimization loop - 3 sample scripts (H2 molecule, optimizer comparison, Heisenberg model) - VQE_README.md: setup guide with algorithm explanation (Chinese) - Compatible with Python 3.11/3.12/3.13 Author: Bai --- .../pyqpanda_alg/VQE/VQE_README.md | 155 +++++++++++ .../pyqpanda_alg/VQE/__init__.py | 28 ++ pyqpanda-algorithm/pyqpanda_alg/VQE/ansatz.py | 135 ++++++++++ .../pyqpanda_alg/VQE/hamiltonian.py | 141 ++++++++++ .../pyqpanda_alg/VQE/measurement.py | 157 +++++++++++ .../pyqpanda_alg/VQE/sample_01_h2_molecule.py | 80 ++++++ .../VQE/sample_02_optimizer_comparison.py | 102 ++++++++ .../VQE/sample_03_custom_hamiltonian.py | 122 +++++++++ .../pyqpanda_alg/VQE/vqe_solver.py | 243 ++++++++++++++++++ pyqpanda-algorithm/pyqpanda_alg/__init__.py | 2 +- 10 files changed, 1164 insertions(+), 1 deletion(-) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/VQE_README.md create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/__init__.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/ansatz.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/hamiltonian.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/measurement.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/sample_01_h2_molecule.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/sample_02_optimizer_comparison.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/sample_03_custom_hamiltonian.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQE/vqe_solver.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/VQE_README.md b/pyqpanda-algorithm/pyqpanda_alg/VQE/VQE_README.md new file mode 100644 index 0000000..576a0bf --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/VQE_README.md @@ -0,0 +1,155 @@ +# VQE 模块 — 变分量子本征求解器 + +> Author: Bai + +--- + +## 重要:请先安装缺失的依赖 + +项目 `requirements.txt` **未包含**所有运行时依赖,使用前请先执行: + +```bash +# 系统库(pyqpanda3 的 C++ 后端需要 OpenMP 支持) +# sudo apt-get install -y libgomp1 # Ubuntu/Debian +# sudo yum install -y libgomp # Amazon Linux / RHEL + +# requirements.txt 中遗漏但其他模块需要的 Python 包 +pip install pandas scikit-learn +``` + +--- + +## 算法原理 + +### 目标 + +VQE 的目标是:**求一个哈密顿量 H 的最小本征值(基态能量)**。 + +最经典的应用场景:求解分子的基态能量。 + +数学表述:E0 = minθ<ψ(θ)|H|ψ(θ)> + + +其中: +- H是系统哈密顿量(一个 Hermitian 矩阵) +- ∣ψ(θ)⟩是参数化量子态(由量子线路产生) +- θ是待优化的经典参数 + +### 核心思想:变分原理 + +**变分原理**保证:对于任意试探态∣ψ⟩,其能量期望值一定 ≥ 基态能量:⟨ψ∣H∣ψ⟩≥E0 +​ + +所以我们只需要不断调整θ,使得能量期望值越来越低,最终逼近E0 + +### 算法整体流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VQE 混合量子-经典循环 │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 经典优化器 │────▶│ 参数化量子线路 │────▶│ 测量期望值 │ +│ │ (更新 θ) │◀────│ (Ansatz) │ │ ⟨ψ(θ)|H|ψ(θ)⟩│ │ +│ └──────────┘ └──────────────┘ └──────────────┘ │ +│ ▲ │ │ +│ │ 能量值 E(θ) │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ 收敛条件: |E(θ_new) - E(θ_old)| < ε │ +└─────────────────────────────────────────────────────────────┘ +``` + +**四大核心组件:** + +| 组件 | 作用 | 对应文件 | +|------|------|----------| +| 哈密顿量 | 将物理系统表示为 Pauli 算符的加权和 | hamiltonian.py | +| 参数化线路 | 生成试探量子态 | ansatz.py | +| 期望值测量 | 在量子硬件上计算 | measurement.py | +| 经典优化器 | 调整参数使能量最小 | vqe_solver.py | + + +### 参数化线路 (Ansatz) + +本模块提供两种 Ansatz: + +**Hardware Efficient Ansatz (HEA)**:通用、硬件友好 +``` +每层结构:RY旋转 → RZ旋转 → CNOT纠缠链 +参数数量 = 量子比特数 × 2 × 层数 +``` + +**RY-Linear Ansatz**:更简单,仅用 RY 门 +``` +每层结构:RY旋转 → CNOT纠缠链 +参数数量 = 量子比特数 × 层数 +``` + +## 环境要求 + +- Python 3.11 / 3.12 / 3.13 +- pyqpanda3 >= 0.3.5 +- numpy, scipy, matplotlib, sympy + +### 哈密顿量功能测试(不依赖量子模拟器) + +```bash +python3 -c " +from pyqpanda_alg.VQE import PauliHamiltonian +H = PauliHamiltonian() +H.add_term(-1.0523, 'II') +H.add_term(0.3979, 'IZ') +H.add_term(-0.3979, 'ZI') +H.add_term(-0.0112, 'ZZ') +H.add_term(0.1809, 'XX') +print(H) +print(f'量子比特数: {H.num_qubits}') +print(f'精确基态能量: {H.exact_ground_energy():.6f}') +" +``` + +期望输出: +``` +-1.0523 * II +0.3979 * IZ -0.3979 * ZI -0.0112 * ZZ +0.1809 * XX +量子比特数: 2 +精确基态能量: -1.857202 +``` + +### 完整 VQE 求解测试(需要 pyqpanda3 量子模拟器) + +```bash +python3 -c " +from pyqpanda_alg.VQE import VQESolver, PauliHamiltonian + +H = PauliHamiltonian() +H.add_term(-1.0523, 'II') +H.add_term(0.3979, 'IZ') +H.add_term(-0.3979, 'ZI') +H.add_term(-0.0112, 'ZZ') +H.add_term(0.1809, 'XX') + +solver = VQESolver(H, ansatz='hea', num_layers=2, optimizer='COBYLA', shots=4096) +print(solver) +result = solver.run() + +print(f'VQE 能量: {result[\"energy\"]:.6f}') +print(f'精确能量: {H.exact_ground_energy():.6f}') +print(f'误差: {abs(result[\"energy\"] - H.exact_ground_energy()):.6f}') +print(f'迭代次数: {result[\"num_iterations\"]}') +print(f'是否收敛: {result[\"success\"]}') +" +``` + +期望输出: +``` +VQESolver(qubits=2, ansatz='hea', layers=2, optimizer='COBYLA', shots=4096) +VQE 能量: -1.857202 +精确能量: -1.857202 +误差: 0.000000 +迭代次数: 200 +是否收敛: False +``` + +> 说明:`是否收敛: False` 表示 COBYLA 跑满了 200 次迭代上限后停止。实际能量已到达全局最优——优化器很早就找到了最优点,后续迭代是在微调已经足够好的参数。这是正常现象。 + diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/__init__.py new file mode 100644 index 0000000..d90ff4e --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/__init__.py @@ -0,0 +1,28 @@ +''' +Variational Quantum Eigensolver (VQE) Module. + +Provides tools for finding the ground state energy of a quantum system +using hybrid quantum-classical optimization. + +Core components: + - VQESolver: main solver class + - PauliHamiltonian: Hamiltonian representation as sum of Pauli terms + - hardware_efficient_ansatz: commonly used parameterized circuit + - measure_expectation: Pauli expectation value measurement + +Author: Bai +''' + +from .hamiltonian import PauliHamiltonian +from .ansatz import hardware_efficient_ansatz, ry_linear_ansatz +from .measurement import measure_expectation, compute_energy +from .vqe_solver import VQESolver + +__all__ = [ + 'VQESolver', + 'PauliHamiltonian', + 'hardware_efficient_ansatz', + 'ry_linear_ansatz', + 'measure_expectation', + 'compute_energy', +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/ansatz.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/ansatz.py new file mode 100644 index 0000000..e099bc4 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/ansatz.py @@ -0,0 +1,135 @@ +""" +Parameterized quantum circuits (Ansatz) for VQE. +Author: Bai +""" + +import numpy as np +from pyqpanda3.core import QCircuit, RY, RZ, CNOT, X + + +def hardware_efficient_ansatz(qubits, params, num_layers=2): + """ + Hardware Efficient Ansatz (HEA). + + Structure per layer: + 1. RY rotation on each qubit (parameterized) + 2. RZ rotation on each qubit (parameterized) + 3. Linear CNOT entanglement (qubit[i] -> qubit[i+1]) + + Total parameter count: num_qubits * 2 * num_layers + + Parameters + qubits : list[int] + Qubit indices, e.g. [0, 1, 2, 3]. + params : array_like + Parameter vector. Length = len(qubits) * 2 * num_layers. + num_layers : int, default=2 + Number of repetition layers. + + Returns + QCircuit : the constructed parameterized circuit. + + Examples: + .. code-block:: python + + import numpy as np + from pyqpanda_alg.VQE import hardware_efficient_ansatz + + qubits = [0, 1, 2] + params = np.random.uniform(0, 2*np.pi, size=3*2*2) + circuit = hardware_efficient_ansatz(qubits, params, num_layers=2) + """ + n = len(qubits) + expected_params = n * 2 * num_layers + if len(params) != expected_params: + raise ValueError( + f"Expected {expected_params} parameters, got {len(params)}." + ) + + cir = QCircuit() + idx = 0 + + for _ in range(num_layers): + # RY rotation layer + for q in qubits: + cir << RY(q, params[idx]) + idx += 1 + # RZ rotation layer + for q in qubits: + cir << RZ(q, params[idx]) + idx += 1 + # Entanglement layer: linear CNOT chain + for i in range(n - 1): + cir << CNOT(qubits[i], qubits[i + 1]) + + return cir + + +def ry_linear_ansatz(qubits, params, num_layers=2): + """ + Simplified RY-only ansatz with linear entanglement. + + A lighter alternative to HEA — uses only RY gates (no RZ). + Good for beginners and quick prototyping. + + Total parameter count: num_qubits * num_layers + + Parameters + qubits : list[int] + Qubit indices. + params : array_like + Parameter vector. Length = len(qubits) * num_layers. + num_layers : int, default=2 + Number of repetition layers. + + Returns + QCircuit : the constructed parameterized circuit. + + Examples: + .. code-block:: python + + import numpy as np + from pyqpanda_alg.VQE import ry_linear_ansatz + + qubits = [0, 1] + params = np.random.uniform(0, 2*np.pi, size=2*2) + circuit = ry_linear_ansatz(qubits, params, num_layers=2) + """ + n = len(qubits) + expected_params = n * num_layers + if len(params) != expected_params: + raise ValueError( + f"Expected {expected_params} parameters, got {len(params)}." + ) + + cir = QCircuit() + idx = 0 + + for _ in range(num_layers): + for q in qubits: + cir << RY(q, params[idx]) + idx += 1 + for i in range(n - 1): + cir << CNOT(qubits[i], qubits[i + 1]) + + return cir + + +def param_count(num_qubits, num_layers, ansatz_type='hea'): + """ + Calculate total number of parameters for a given ansatz configuration. + + Parameters + num_qubits : int + num_layers : int + ansatz_type : str, 'hea' or 'ry_linear' + + Returns + int : total parameter count + """ + if ansatz_type == 'hea': + return num_qubits * 2 * num_layers + elif ansatz_type == 'ry_linear': + return num_qubits * num_layers + else: + raise ValueError(f"Unknown ansatz type: {ansatz_type}") diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/hamiltonian.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/hamiltonian.py new file mode 100644 index 0000000..04f8d38 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/hamiltonian.py @@ -0,0 +1,141 @@ +""" +Pauli Hamiltonian representation for VQE. + +A Hamiltonian is expressed as a weighted sum of Pauli strings: + H = c0*P0 + c1*P1 + ... + cn*Pn +where each Pi is a tensor product of {I, X, Y, Z}. + +Author: Bai +""" + +import numpy as np + + +class PauliHamiltonian: + """ + Represents a Hamiltonian as a linear combination of Pauli operators. + + Parameters + None (use add_term to build the Hamiltonian) + + Examples: + .. code-block:: python + + from pyqpanda_alg.VQE import PauliHamiltonian + + # H2 molecule Hamiltonian (STO-3G, bond length 0.735 Angstrom) + H = PauliHamiltonian() + H.add_term(-1.0523, "II") + H.add_term(0.3979, "IZ") + H.add_term(-0.3979, "ZI") + H.add_term(-0.0112, "ZZ") + H.add_term(0.1809, "XX") + + print(H) + print("Qubits:", H.num_qubits) + """ + + VALID_PAULIS = {'I', 'X', 'Y', 'Z'} + + def __init__(self): + self.terms = [] + + def add_term(self, coeff, pauli_str): + """ + Add a weighted Pauli term. + + Parameters + coeff : float + Real coefficient for this term. + pauli_str : str + String of Pauli operators, e.g. "IXYZ". + Length must be consistent across all terms. + """ + pauli_str = pauli_str.upper() + # validate characters + for ch in pauli_str: + if ch not in self.VALID_PAULIS: + raise ValueError( + f"Invalid Pauli character '{ch}'. Must be I, X, Y, or Z." + ) + # validate length consistency + if self.terms and len(pauli_str) != len(self.terms[0][1]): + raise ValueError( + f"Pauli string length {len(pauli_str)} does not match " + f"existing length {len(self.terms[0][1])}." + ) + self.terms.append((float(coeff), pauli_str)) + + @property + def num_qubits(self): + """Number of qubits in this Hamiltonian.""" + if not self.terms: + return 0 + return len(self.terms[0][1]) + + @property + def num_terms(self): + """Number of Pauli terms.""" + return len(self.terms) + + def identity_offset(self): + """Sum of coefficients for all-identity terms (constant energy offset).""" + offset = 0.0 + for coeff, pauli_str in self.terms: + if all(ch == 'I' for ch in pauli_str): + offset += coeff + return offset + + def non_identity_terms(self): + """Return terms that contain at least one non-I Pauli operator.""" + return [ + (coeff, ps) for coeff, ps in self.terms + if not all(ch == 'I' for ch in ps) + ] + + def to_matrix(self): + """ + Convert Hamiltonian to its full matrix representation (for small systems). + Useful for verifying VQE results against exact diagonalization. + + Returns + ndarray of shape (2^n, 2^n) + """ + n = self.num_qubits + dim = 2 ** n + H_mat = np.zeros((dim, dim), dtype=complex) + + # single-qubit Pauli matrices + pauli_mats = { + 'I': np.eye(2, dtype=complex), + 'X': np.array([[0, 1], [1, 0]], dtype=complex), + 'Y': np.array([[0, -1j], [1j, 0]], dtype=complex), + 'Z': np.array([[1, 0], [0, -1]], dtype=complex), + } + + for coeff, pauli_str in self.terms: + term_mat = np.array([[1.0]], dtype=complex) + for ch in pauli_str: + term_mat = np.kron(term_mat, pauli_mats[ch]) + H_mat += coeff * term_mat + + return H_mat + + def exact_ground_energy(self): + """ + Compute exact ground state energy via numpy diagonalization. + Only practical for small qubit counts (<=12). + """ + H_mat = self.to_matrix() + eigenvalues = np.linalg.eigvalsh(H_mat) + return eigenvalues[0] + + def __repr__(self): + lines = [] + for i, (coeff, ps) in enumerate(self.terms): + sign = '+' if coeff >= 0 and i > 0 else '' + lines.append(f"{sign}{coeff:.4f} * {ps}") + return ' '.join(lines) if lines else "Empty Hamiltonian" + + def __len__(self): + return len(self.terms) diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/measurement.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/measurement.py new file mode 100644 index 0000000..32ae4a9 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/measurement.py @@ -0,0 +1,157 @@ +""" +Expectation value measurement for Pauli Hamiltonians. + +To measure ⟨ψ|H|ψ⟩, we decompose H into Pauli terms and compute each +term's expectation value via basis rotation + probability readout: + - Z: measure directly in computational basis + - X: apply H gate before measurement (rotates X-basis to Z-basis) + - Y: apply Sdg then H before measurement (rotates Y-basis to Z-basis) + - I: contributes +1 always, no measurement needed + +Author: Bai +""" + +import numpy as np +from pyqpanda3.core import QCircuit, QProg, CPUQVM, H as HGate, RZ + + +def _basis_rotation_circuit(qubits, pauli_str): + """ + Build circuit that rotates measurement basis for a Pauli string. + + For each qubit position: + 'Z' or 'I' -> no gate needed + 'X' -> H gate (maps X-eigenstates to Z-eigenstates) + 'Y' -> RZ(-π/2) + H (maps Y-eigenstates to Z-eigenstates) + + Parameters + qubits : list[int] + Qubit indices. + pauli_str : str + Pauli string, e.g. "IXYZ". + + Returns + QCircuit : basis rotation circuit. + """ + cir = QCircuit() + for i, pauli in enumerate(pauli_str): + if pauli == 'X': + cir << HGate(qubits[i]) + elif pauli == 'Y': + cir << RZ(qubits[i], -np.pi / 2) + cir << HGate(qubits[i]) + # 'Z' and 'I' need no rotation + return cir + + +def measure_expectation(circuit, qubits, pauli_str, shots=1024): + """ + Measure expectation value of a single Pauli string. + + Uses probability distribution from the simulator. The parity of + measured bits (on non-I positions) determines the ±1 eigenvalue: + eigenvalue(b) = (-1)^(count of '1' bits at non-I positions in b) + + Parameters + circuit : QCircuit + The state preparation circuit (ansatz with parameters applied). + qubits : list[int] + Qubit indices used in the circuit. + pauli_str : str + Pauli operator string, e.g. "XZ", "IY". + shots : int, default=1024 + Number of samples for adding statistical noise. + If shots <= 0, uses exact probabilities (no noise). + + Returns + float : estimated expectation value in range [-1, +1]. + + Examples: + .. code-block:: python + + expectation = measure_expectation(ansatz_circuit, [0,1], "ZZ") + """ + n = len(qubits) + if len(pauli_str) != n: + raise ValueError( + f"Pauli string length ({len(pauli_str)}) must match " + f"qubit count ({n})." + ) + + # positions where we actually need to check parity + active_positions = [i for i, p in enumerate(pauli_str) if p != 'I'] + if not active_positions: + return 1.0 + + # build full program: ansatz + basis rotation + prog = QProg(n) + qvec = prog.qubits() + + prog << circuit + prog << _basis_rotation_circuit(qvec, pauli_str) + + # run on CPUQVM simulator to get probability distribution + machine = CPUQVM() + machine.run(prog, shots) + prob_list = machine.result().get_prob_list(qvec) + + # compute expectation from probability distribution + expectation = 0.0 + for state_idx, prob in enumerate(prob_list): + if prob < 1e-15: + continue + # extract bits and compute parity on active positions + parity = 0 + for pos in active_positions: + # bit at position pos in the binary repr of state_idx + # qubit ordering: pos=0 is most significant bit + bit_val = (state_idx >> (n - 1 - pos)) & 1 + parity += bit_val + sign = (-1) ** (parity % 2) + expectation += sign * prob + + return expectation + + +def compute_energy(circuit, qubits, hamiltonian, shots=1024): + """ + Compute total energy ⟨ψ|H|ψ⟩ for a full Pauli Hamiltonian. + + Iterates over all Pauli terms, measures each, and sums weighted results: + E = Σ ci * ⟨Pi⟩ + + Parameters + circuit : QCircuit + State preparation circuit (ansatz). + qubits : list[int] + Qubit indices. + hamiltonian : PauliHamiltonian + The Hamiltonian to evaluate. + shots : int, default=1024 + Measurement shots per Pauli term. + + Returns + float : estimated total energy. + + Examples: + .. code-block:: python + + from pyqpanda_alg.VQE import PauliHamiltonian, compute_energy + + H = PauliHamiltonian() + H.add_term(-1.05, "II") + H.add_term(0.39, "ZZ") + + energy = compute_energy(ansatz_circuit, [0, 1], H) + """ + energy = 0.0 + + for coeff, pauli_str in hamiltonian.terms: + # all-I terms don't need quantum measurement + if all(ch == 'I' for ch in pauli_str): + energy += coeff + else: + exp_val = measure_expectation(circuit, qubits, pauli_str, shots) + energy += coeff * exp_val + + return energy diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_01_h2_molecule.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_01_h2_molecule.py new file mode 100644 index 0000000..933eb8b --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_01_h2_molecule.py @@ -0,0 +1,80 @@ +""" +示例 1:用 VQE 求解 H₂ 分子基态能量 + +场景:量子化学中最经典的入门问题 —— 求氢分子 (H₂) 的基态能量。 + 已知 H₂ 在 STO-3G 基组、键长 0.735 Å 时的精确基态能量为 -1.857 Ha。 + 我们用 VQE 在量子模拟器上逼近这个值。 + +运行方式: +$ source .venv/bin/activate +$ python3 sample_01_h2_molecule.py + +Author: Bai +""" + +import numpy as np +from pyqpanda_alg.VQE import VQESolver, PauliHamiltonian + + +def build_h2_hamiltonian(): + """ + 构建 H₂ 分子的哈密顿量(STO-3G 基组,键长 0.735 Å)。 + 该哈密顿量已经过 Jordan-Wigner 变换,映射为 2 量子比特 Pauli 算符。 + """ + H = PauliHamiltonian() + H.add_term(-1.0523, "II") # 常数项(核排斥能 + 电子贡献) + H.add_term(0.3979, "IZ") # 单体项 + H.add_term(-0.3979, "ZI") # 单体项 + H.add_term(-0.0112, "ZZ") # 双体项(电子-电子相互作用) + H.add_term(0.1809, "XX") # 双体项(电子跃迁) + return H + + +def main(): + # 第一步:构建哈密顿量 + H = build_h2_hamiltonian() + print("=== H₂ 分子 VQE 求解 ===") + print(f"哈密顿量: {H}") + print(f"量子比特数: {H.num_qubits}") + print(f"Pauli 项数: {H.num_terms}") + print() + + # 第二步:精确对角化(用作对比基准) + exact_energy = H.exact_ground_energy() + print(f"精确基态能量(numpy对角化): {exact_energy:.6f} Ha") + print() + + # 第三步:VQE 求解 + solver = VQESolver( + hamiltonian=H, + ansatz='hea', # Hardware Efficient Ansatz + num_layers=2, # 2 层参数化线路 + optimizer='COBYLA', # 无梯度优化器 + shots=4096, # 测量次数 + maxiter=200, # 最大迭代次数 + ) + print(f"求解器配置: {solver}") + print("正在优化...") + + result = solver.run() + + # 第四步:输出结果 + print() + print("=== 结果 ===") + print(f"VQE 基态能量: {result['energy']:.6f} Ha") + print(f"精确基态能量: {exact_energy:.6f} Ha") + print(f"绝对误差: {abs(result['energy'] - exact_energy):.6f} Ha") + print(f"迭代次数: {result['num_iterations']}") + print(f"优化器收敛: {result['success']}") + print() + + # 第五步:展示能量收敛趋势(取前20步) + history = result['history'] + print("=== 能量收敛过程(前 20 步)===") + for i, e in enumerate(history[:20]): + bar = "█" * max(1, int((e - exact_energy) * 50)) + print(f" Step {i+1:3d}: {e:+.4f} {bar}") + + +if __name__ == "__main__": + main() diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_02_optimizer_comparison.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_02_optimizer_comparison.py new file mode 100644 index 0000000..74ee9a2 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_02_optimizer_comparison.py @@ -0,0 +1,102 @@ +""" +示例 2:对比不同优化器和 Ansatz 对 VQE 收敛速度的影响 + +场景:同一个哈密顿量,用不同配置运行 VQE,观察: + - 不同优化器(COBYLA vs Powell vs Nelder-Mead)的收敛差异 + - 不同 Ansatz(HEA vs RY-linear)的表达能力差异 + 帮助学员理解"选择什么优化策略"对结果质量的影响。 + +运行方式: +$ source .venv/bin/activate +$ python3 sample_02_optimizer_comparison.py + +Author: Bai +""" + +import numpy as np +from pyqpanda_alg.VQE import VQESolver, PauliHamiltonian + + +def build_hamiltonian(): + """构建一个 2-qubit 测试哈密顿量(H₂ 简化模型)""" + H = PauliHamiltonian() + H.add_term(-1.0523, "II") + H.add_term(0.3979, "IZ") + H.add_term(-0.3979, "ZI") + H.add_term(-0.0112, "ZZ") + H.add_term(0.1809, "XX") + return H + + +def run_experiment(H, ansatz, optimizer, num_layers=2): + """运行一次 VQE 实验,返回结果摘要""" + # 固定随机种子,确保初始参数一致(公平对比) + np.random.seed(42) + + solver = VQESolver( + hamiltonian=H, + ansatz=ansatz, + num_layers=num_layers, + optimizer=optimizer, + shots=4096, + maxiter=150, + ) + result = solver.run() + return { + 'ansatz': ansatz, + 'optimizer': optimizer, + 'energy': result['energy'], + 'iterations': result['num_iterations'], + 'success': result['success'], + } + + +def main(): + H = build_hamiltonian() + exact = H.exact_ground_energy() + print("=== VQE 优化器 & Ansatz 对比实验 ===") + print(f"目标: H₂ 基态能量 = {exact:.6f} Ha") + print() + + # 实验配置 + experiments = [ + ('hea', 'COBYLA'), + ('hea', 'Powell'), + ('hea', 'Nelder-Mead'), + ('ry_linear', 'COBYLA'), + ('ry_linear', 'Powell'), + ] + + # 运行所有实验 + results = [] + for ansatz, optimizer in experiments: + print(f" 运行中... ansatz={ansatz:10s} optimizer={optimizer:12s}", end="") + r = run_experiment(H, ansatz, optimizer) + error = abs(r['energy'] - exact) + print(f" -> E={r['energy']:+.6f} 误差={error:.6f} 迭代={r['iterations']}") + r['error'] = error + results.append(r) + + # 打印汇总表 + print() + print("=" * 72) + print(f"{'Ansatz':<12} {'优化器':<14} {'能量 (Ha)':<12} {'误差':<10} {'迭代次数'}") + print("-" * 72) + for r in results: + print(f"{r['ansatz']:<12} {r['optimizer']:<14} {r['energy']:+.6f} " + f"{r['error']:.6f} {r['iterations']}") + print("=" * 72) + print() + + # 找出最佳组合 + best = min(results, key=lambda x: x['error']) + print(f"最佳组合: ansatz={best['ansatz']}, optimizer={best['optimizer']}") + print(f"最低误差: {best['error']:.8f} Ha") + print() + print("结论:HEA 比 RY-linear 表达能力更强(参数多一倍),") + print(" 但 RY-linear 线路更浅,在真实量子硬件上噪声更小。") + print(" 优化器选择取决于具体问题——无免费午餐定理。") + + +if __name__ == "__main__": + main() diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_03_custom_hamiltonian.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_03_custom_hamiltonian.py new file mode 100644 index 0000000..f701f79 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/sample_03_custom_hamiltonian.py @@ -0,0 +1,122 @@ +""" +示例 3:自定义哈密顿量 —— 求解 2-qubit 反铁磁 Heisenberg 模型基态 + +场景:凝聚态物理中的经典问题。Heisenberg 模型描述量子自旋链的相互作用, + 其哈密顿量为: + H = J * (XX + YY + ZZ) + 其中 J > 0 表示反铁磁耦合。 + + 基态是自旋单态 (singlet state): |ψ⟩ = (|01⟩ - |10⟩) / √2 + 对应基态能量 E₀ = -3J(精确解可验证)。 + + 本示例展示如何用 VQE 处理任意自定义哈密顿量。 + +运行方式: +$ source .venv/bin/activate +$ python3 sample_03_custom_hamiltonian.py + +Author: Bai +""" + +import numpy as np +from pyqpanda_alg.VQE import VQESolver, PauliHamiltonian + + +def build_heisenberg_hamiltonian(J=1.0): + """ + 构建 2-qubit 反铁磁 Heisenberg 模型哈密顿量。 + + H = J * (X₁X₂ + Y₁Y₂ + Z₁Z₂) + + 参数: + J : float — 耦合强度(J>0 反铁磁,J<0 铁磁) + + 精确基态能量:E₀ = -3J + """ + H = PauliHamiltonian() + H.add_term(J, "XX") # X₁X₂ 相互作用 + H.add_term(J, "YY") # Y₁Y₂ 相互作用 + H.add_term(J, "ZZ") # Z₁Z₂ 相互作用 + return H + + +def build_heisenberg_with_field(J=1.0, h=0.5): + """ + 带外磁场的 Heisenberg 模型(更复杂的情况)。 + + H = J * (XX + YY + ZZ) + h * (ZI + IZ) + + 外磁场 h 破坏了自旋旋转对称性,基态不再是简单的 singlet。 + 此时精确解需要数值对角化,正是 VQE 发挥作用的场景。 + """ + H = PauliHamiltonian() + H.add_term(J, "XX") + H.add_term(J, "YY") + H.add_term(J, "ZZ") + H.add_term(h, "ZI") # 外磁场作用在第 1 个自旋 + H.add_term(h, "IZ") # 外磁场作用在第 2 个自旋 + return H + + +def main(): + print("=== 示例 3: Heisenberg 自旋链 VQE 求解 ===") + print() + + # ----- 实验 A:纯 Heisenberg 模型 ----- + J = 1.0 + H1 = build_heisenberg_hamiltonian(J) + exact1 = H1.exact_ground_energy() + + print(f"[实验 A] 纯 Heisenberg 模型 (J={J})") + print(f" 哈密顿量: H = {J}*(XX + YY + ZZ)") + print(f" 理论基态能量: E₀ = -3J = {-3*J:.4f}") + print(f" numpy 对角化: E₀ = {exact1:.4f}") + + solver1 = VQESolver(H1, ansatz='hea', num_layers=2, optimizer='COBYLA', shots=4096) + result1 = solver1.run() + + print(f" VQE 结果: E = {result1['energy']:.4f}") + print(f" 误差: {abs(result1['energy'] - exact1):.6f}") + print() + + # ----- 实验 B:带外磁场 ----- + h = 0.5 + H2 = build_heisenberg_with_field(J, h) + exact2 = H2.exact_ground_energy() + + print(f"[实验 B] Heisenberg + 外磁场 (J={J}, h={h})") + print(f" 哈密顿量: H = {J}*(XX + YY + ZZ) + {h}*(ZI + IZ)") + print(f" numpy 对角化: E₀ = {exact2:.6f}") + + solver2 = VQESolver(H2, ansatz='hea', num_layers=3, optimizer='COBYLA', shots=4096) + result2 = solver2.run() + + print(f" VQE 结果: E = {result2['energy']:.6f}") + print(f" 误差: {abs(result2['energy'] - exact2):.6f}") + print() + + # ----- 实验 C:扫描耦合强度 J ----- + print("[实验 C] 扫描耦合强度 J,观察基态能量变化") + print(f" {'J':<6} {'精确 E₀':<12} {'VQE E':<12} {'误差'}") + print(" " + "-" * 44) + + for J_val in [0.2, 0.5, 1.0, 1.5, 2.0]: + H_scan = build_heisenberg_hamiltonian(J_val) + exact_scan = H_scan.exact_ground_energy() + + np.random.seed(0) # 固定种子保证可复现 + solver_scan = VQESolver( + H_scan, ansatz='ry_linear', num_layers=2, + optimizer='COBYLA', shots=4096, maxiter=100 + ) + r = solver_scan.run() + error = abs(r['energy'] - exact_scan) + print(f" {J_val:<6.1f} {exact_scan:<12.4f} {r['energy']:<12.4f} {error:.6f}") + + print() + print("结论:VQE 能准确追踪不同参数下的基态能量,") + print(" 展示了其作为通用量子求解器的灵活性。") + + +if __name__ == "__main__": + main() diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQE/vqe_solver.py b/pyqpanda-algorithm/pyqpanda_alg/VQE/vqe_solver.py new file mode 100644 index 0000000..262ed9c --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQE/vqe_solver.py @@ -0,0 +1,243 @@ +""" +VQE Solver — the main variational quantum eigensolver class. + +Implements the hybrid quantum-classical optimization loop: + 1. Prepare trial state |ψ(θ)⟩ via parameterized ansatz + 2. Measure energy E(θ) = ⟨ψ(θ)|H|ψ(θ)⟩ + 3. Update θ with classical optimizer to minimize E(θ) + 4. Repeat until convergence + +Author: Bai +""" + +import numpy as np +from scipy.optimize import minimize as scipy_minimize + +from .hamiltonian import PauliHamiltonian +from .ansatz import hardware_efficient_ansatz, ry_linear_ansatz, param_count +from .measurement import compute_energy + + +class VQESolver: + """ + Variational Quantum Eigensolver. + + Finds the minimum eigenvalue (ground state energy) of a Hamiltonian + using parameterized quantum circuits and classical optimization. + + Parameters + hamiltonian : PauliHamiltonian + Target Hamiltonian expressed as sum of Pauli terms. + ansatz : str or callable, default='hea' + Ansatz type. Built-in options: 'hea' (Hardware Efficient), + 'ry_linear' (RY-only). Or pass a custom callable: + func(qubits, params) -> QCircuit. + num_layers : int, default=2 + Number of ansatz repetition layers. + optimizer : str, default='COBYLA' + Classical optimizer. Supports any scipy.optimize.minimize method: + 'COBYLA', 'Nelder-Mead', 'Powell', 'SLSQP', 'L-BFGS-B'. + shots : int, default=1024 + Measurement shots per energy evaluation. + tol : float, default=1e-6 + Convergence tolerance for the optimizer. + maxiter : int, default=200 + Maximum number of optimizer iterations. + + Attributes + energy_history : list[float] + Energy value recorded at each optimizer iteration. + optimal_params : ndarray or None + Optimized parameter vector after calling run(). + ground_energy : float or None + Final optimized energy value after calling run(). + + Examples: + .. code-block:: python + + from pyqpanda_alg.VQE import VQESolver, PauliHamiltonian + + # Build H2 molecule Hamiltonian + H = PauliHamiltonian() + H.add_term(-1.0523, "II") + H.add_term( 0.3979, "IZ") + H.add_term(-0.3979, "ZI") + H.add_term(-0.0112, "ZZ") + H.add_term( 0.1809, "XX") + + # Run VQE + solver = VQESolver(H, ansatz='hea', num_layers=2) + result = solver.run() + + print(f"VQE ground energy: {result['energy']:.6f}") + print(f"Exact energy: {H.exact_ground_energy():.6f}") + print(f"Iterations: {result['num_iterations']}") + """ + + def __init__(self, hamiltonian, ansatz='hea', num_layers=2, + optimizer='COBYLA', shots=1024, tol=1e-6, maxiter=200): + + if not isinstance(hamiltonian, PauliHamiltonian): + raise TypeError("hamiltonian must be a PauliHamiltonian instance.") + if hamiltonian.num_qubits == 0: + raise ValueError("Hamiltonian has no terms. Add terms first.") + + self.hamiltonian = hamiltonian + self.num_qubits = hamiltonian.num_qubits + self.num_layers = num_layers + self.optimizer = optimizer + self.shots = shots + self.tol = tol + self.maxiter = maxiter + + # resolve ansatz + self._ansatz_name = ansatz if isinstance(ansatz, str) else 'custom' + self._ansatz_fn = self._resolve_ansatz(ansatz) + self._num_params = self._compute_num_params() + + # results (populated after run()) + self.energy_history = [] + self.optimal_params = None + self.ground_energy = None + + def _resolve_ansatz(self, ansatz): + """Map ansatz specification to a callable.""" + if callable(ansatz): + return ansatz + elif ansatz == 'hea': + return lambda qubits, params: hardware_efficient_ansatz( + qubits, params, self.num_layers + ) + elif ansatz == 'ry_linear': + return lambda qubits, params: ry_linear_ansatz( + qubits, params, self.num_layers + ) + else: + raise ValueError( + f"Unknown ansatz '{ansatz}'. Use 'hea', 'ry_linear', " + f"or a custom callable." + ) + + def _compute_num_params(self): + """Determine total parameter count based on ansatz type.""" + if self._ansatz_name == 'hea': + return param_count(self.num_qubits, self.num_layers, 'hea') + elif self._ansatz_name == 'ry_linear': + return param_count(self.num_qubits, self.num_layers, 'ry_linear') + else: + # for custom ansatz, user must provide init_params in run() + return None + + def _cost_function(self, params): + """ + Objective function for the optimizer. + Builds ansatz circuit with given params, then measures energy. + """ + qubits = list(range(self.num_qubits)) + circuit = self._ansatz_fn(qubits, params) + energy = compute_energy(circuit, qubits, self.hamiltonian, self.shots) + self.energy_history.append(energy) + return energy + + def run(self, init_params=None): + """ + Execute the VQE optimization loop. + + Parameters + init_params : array_like or None + Initial parameter values. If None, uses random initialization + in range [0, π]. Must be provided if using a custom ansatz + with unknown parameter count. + + Returns + dict with keys: + 'energy' : float — optimized ground state energy + 'optimal_params' : ndarray — best parameters found + 'num_iterations' : int — number of cost function evaluations + 'history' : list[float] — energy at each iteration + 'success' : bool — whether optimizer converged + """ + # initialize parameters + if init_params is not None: + x0 = np.asarray(init_params, dtype=float) + elif self._num_params is not None: + x0 = np.random.uniform(0, np.pi, size=self._num_params) + else: + raise ValueError( + "Custom ansatz requires init_params to be provided." + ) + + # reset history + self.energy_history = [] + + # run scipy optimizer + result = scipy_minimize( + self._cost_function, + x0, + method=self.optimizer, + tol=self.tol, + options={'maxiter': self.maxiter} + ) + + self.optimal_params = result.x + self.ground_energy = result.fun + + return { + 'energy': result.fun, + 'optimal_params': result.x, + 'num_iterations': len(self.energy_history), + 'history': self.energy_history, + 'success': result.success, + } + + def run_with_callback(self, init_params=None, callback=None): + """ + Run VQE with a user-defined callback after each iteration. + + Parameters + init_params : array_like or None + Initial parameters (same as run()). + callback : callable or None + Called as callback(params) after each optimizer step. + Useful for logging or visualization. + + Returns + dict (same structure as run()) + """ + if init_params is not None: + x0 = np.asarray(init_params, dtype=float) + elif self._num_params is not None: + x0 = np.random.uniform(0, np.pi, size=self._num_params) + else: + raise ValueError( + "Custom ansatz requires init_params to be provided." + ) + + self.energy_history = [] + + result = scipy_minimize( + self._cost_function, + x0, + method=self.optimizer, + tol=self.tol, + options={'maxiter': self.maxiter}, + callback=callback, + ) + + self.optimal_params = result.x + self.ground_energy = result.fun + + return { + 'energy': result.fun, + 'optimal_params': result.x, + 'num_iterations': len(self.energy_history), + 'history': self.energy_history, + 'success': result.success, + } + + def __repr__(self): + return ( + f"VQESolver(qubits={self.num_qubits}, ansatz='{self._ansatz_name}', " + f"layers={self.num_layers}, optimizer='{self.optimizer}', " + f"shots={self.shots})" + ) diff --git a/pyqpanda-algorithm/pyqpanda_alg/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/__init__.py index 12d6808..6e99e2f 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/__init__.py @@ -30,7 +30,7 @@ warnings.filterwarnings("ignore", category=SyntaxWarning) from . import QAOA -# from . import VQE +from . import VQE # from . import HHL # from . import QAOA from . import QARM