From 33e1a265f7ebb5a285424b5da4fca25b39bf6b8b Mon Sep 17 00:00:00 2001 From: stan Date: Tue, 22 Sep 2026 14:03:38 +0800 Subject: [PATCH 1/2] fix(qaoa): initialize Hamiltonian inputs with their full register width --- pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py | 16 ++- test/QAOA/Test_qaoa_QAOA_calculate_energy.py | 144 +++++++++++++------ 2 files changed, 112 insertions(+), 48 deletions(-) diff --git a/pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py b/pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py index 688e01e3..63c1acc3 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py +++ b/pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py @@ -252,9 +252,9 @@ class QAOA: optimization problem. Parameters - problem : ``expression`` in sympy or ``pq.PauliOperator``\n - A polynomial function with binary variables to be optimized. Support an expression in sympy. Next version will - support an object from pypanda PauliOperator. + problem : sympy expression, ``PauliOperator`` or ``Hamiltonian``\n + A binary polynomial or a diagonal Pauli Z Hamiltonian. Operator + inputs use PyQPanda3 and retain their explicit qubit indices. init_circuit : ``function``, ``optional``\n The quantum circuit to create the initial state of QAOA algorithm. Default is Hadamard circuit to create an @@ -273,6 +273,10 @@ class QAOA: The dict which stores the function value for solutions being sampled during the optimization. problem_dimension : ``integer``\n The problem dimension, and also the qubit number. + For PauliOperator or Hamiltonian input, this is one plus the + highest referenced qubit index (zero for identity-only input). + Gaps in the addresses are retained, so bit vectors and custom + circuits use the original operator indices. circuit iter : ``integer``\n The number of times the quantum circuit being called during optimization. @@ -318,9 +322,9 @@ def __init__(self, problem, init_circuit=None, self.problem = problem.pauli_operator() self.operator = problem.pauli_operator() qubit = set() - for term in problem.terms(): + for term in self.operator.terms(): qubit = qubit |set([qubit.qbit() for qubit in term.paulis()]) - problem_dimension = len(qubit) + problem_dimension = max(qubit, default=-1) + 1 elif isinstance(problem, PauliOperator): self.problem = problem @@ -328,7 +332,7 @@ def __init__(self, problem, init_circuit=None, qubit = set() for term in problem.terms(): qubit = qubit |set([qubit.qbit() for qubit in term.paulis()]) - problem_dimension = len(qubit) + problem_dimension = max(qubit, default=-1) + 1 else: raise TypeError("problem must be a sympy expression or a PauliOperator") diff --git a/test/QAOA/Test_qaoa_QAOA_calculate_energy.py b/test/QAOA/Test_qaoa_QAOA_calculate_energy.py index 7b20a19f..f0dfad98 100644 --- a/test/QAOA/Test_qaoa_QAOA_calculate_energy.py +++ b/test/QAOA/Test_qaoa_QAOA_calculate_energy.py @@ -1,42 +1,102 @@ -# import pytest -# import sys -# from pathlib import Path -# import sympy as sp -# import numpy as np -# -# # 添加项目路径 -# sys.path.append((Path.cwd().parent.parent).__str__()) -# -# from pyqpanda_alg.QAOA.qaoa import QAOA, p_1 -# from pyqpanda3.hamiltonian import PauliOperator -# -# -# class TestCalculateEnergy: -# """测试calculate_energy接口""" -# -# def test_basic_functionality_with_symbolic_problem(self): -# """测试基本功能:使用符号问题计算能量""" -# # 创建测试问题:f = 2*x0*x1 + 3*x2 - 1 -# vars = sp.symbols('x0:3') -# f = 2*vars[0]*vars[1] + 3*vars[2] - 1 -# -# # 初始化QAOA -# qaoa_f = QAOA(f) -# -# # 测试不同的解 -# test_cases = [ -# ([1, 0, 0], 2*1*0 + 3*0 - 1), # f(1,0,0) = -1 -# ([0, 1, 1], 2*0*1 + 3*1 - 1), # f(0,1,1) = 2 -# ([1, 1, 0], 2*1*1 + 3*0 - 1), # f(1,1,0) = 1 -# ([0, 0, 0], 2*0*0 + 3*0 - 1), # f(0,0,0) = -1 -# ([1, 1, 1], 2*1*1 + 3*1 - 1), # f(1,1,1) = 4 -# ] -# -# for solution, expected_energy in test_cases: -# calculated_energy = qaoa_f.calculate_energy(solution) -# assert abs(calculated_energy - expected_energy) < 1e-10, \ -# f"解 {solution} 的能量计算错误: 期望 {expected_energy}, 得到 {calculated_energy}" -# -# if __name__ == "__main__": -# # 运行测试 -# pytest.main([__file__, "-v"]) \ No newline at end of file +"""Sparse operator addresses must match QAOA energy and CPU register indices.""" + +import numpy as np +import pytest +from pyqpanda3.core import QCircuit, X +from pyqpanda3.hamiltonian import Hamiltonian, PauliOperator +from pyqpanda_alg.QAOA.qaoa import QAOA + +CASES = [ + ({"Z2": 1.25, "": -0.5}, 3), + ({"Z0 Z3": 0.75, "Z3": -0.25, "": 0.3}, 4), + ({"Z1 Z4": 0.7, "Z4": -0.2, "": 0.1}, 5), + ({"Z10": -0.9, "": 0.2}, 11), + ({"Z0": 0.1, "Z1": 0.2}, 2), +] + + +def _energies(terms: dict[str, float], width: int) -> np.ndarray: + """Evaluate Z parities directly on integer basis states.""" + indices = np.arange(1 << width) + energies = np.zeros(1 << width) + for word, coefficient in terms.items(): + signs = np.ones(1 << width) + for pauli in word.split(): + signs *= 1 - 2 * ((indices >> int(pauli[1:])) & 1) + energies += coefficient * signs + return energies + + +def _reference_probabilities( + energies: np.ndarray, width: int, gammas: list[float], betas: list[float] +) -> np.ndarray: + """Apply diagonal phases and analytic RX(-2 beta), without SDK circuits.""" + state = np.ones(1 << width, dtype=complex) / np.sqrt(1 << width) + indices = np.arange(1 << width) + for gamma, beta in zip(gammas, betas, strict=True): + state *= np.exp(-1j * gamma * energies) + for qubit in range(width): + state = ( + np.cos(beta) * state + 1j * np.sin(beta) * state[indices ^ (1 << qubit)] + ) + return np.abs(state) ** 2 + + +@pytest.mark.parametrize("wrapper", [PauliOperator, Hamiltonian]) +@pytest.mark.parametrize(("terms", "width"), CASES) +@pytest.mark.parametrize( + ("gammas", "betas"), [([0.31], [0.17]), ([0.31, -0.13], [0.17, 0.29])] +) +def test_sparse_operator_energy_and_cpu_probabilities( + wrapper: type, + terms: dict[str, float], + width: int, + gammas: list[float], + betas: list[float], +) -> None: + """Retain leading/internal holes, high addresses and dense compatibility.""" + problem = QAOA(wrapper(terms)) + assert problem.problem_dimension == width + energies = _energies(terms, width) + for index, expected in enumerate(energies): + bits = [(index >> qubit) & 1 for qubit in range(width)] + assert problem.calculate_energy(bits) == pytest.approx(expected) + actual = problem.run_qaoa_circuit(gammas, betas, shots=-1) + assert all(len(key) == width for key in actual) + probabilities = np.array( + [actual.get(format(i, f"0{width}b"), 0.0) for i in range(1 << width)] + ) + np.testing.assert_allclose( + probabilities, + _reference_probabilities(energies, width, gammas, betas), + atol=1e-12, + rtol=0, + ) + + +@pytest.mark.parametrize("wrapper", [PauliOperator, Hamiltonian]) +def test_custom_circuits_receive_original_qubit_addresses(wrapper: type) -> None: + """Do not compact addresses when passing the register to user circuits.""" + seen = [] + + def initial(qubits: list[int]) -> QCircuit: + seen.append(list(qubits)) + return QCircuit() << X(qubits[3]) + + problem = QAOA( + wrapper({"Z3": 1.0}), + init_circuit=initial, + mixer_circuit=lambda qubits, angle: QCircuit(), + ) + actual = problem.run_qaoa_circuit([0.23], [0.17], shots=-1) + assert seen == [[0, 1, 2, 3]] + assert actual.get("1000", 0.0) == pytest.approx(1.0) + assert problem.calculate_energy([0, 0, 0, 1]) == -1.0 + + +@pytest.mark.parametrize("wrapper", [PauliOperator, Hamiltonian]) +def test_identity_only_keeps_zero_register_width(wrapper: type) -> None: + """Retain the existing constructor behavior for an empty support.""" + problem = QAOA(wrapper({"": 2.0})) + assert problem.problem_dimension == 0 + assert problem.calculate_energy([]) == 2.0 From c2eaf5e64eea88fa4e001d65689d27772d8cabc0 Mon Sep 17 00:00:00 2001 From: stan Date: Tue, 22 Sep 2026 14:27:49 +0800 Subject: [PATCH 2/2] fix(QUBO): preserve declared variables in QAOA output --- pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py | 12 +++- test/QAlgBase/Test_QUBO_QAOA_register.py | 66 ++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 test/QAlgBase/Test_QUBO_QAOA_register.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py b/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py index 576f1034..126b0c60 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py +++ b/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py @@ -507,9 +507,12 @@ def run(self, layer=None, optimizer='SLSQP', optimizer_option=None): For `TNC` use `maxfun` instead of `maxiter`. Returns - qaoa_result : ``list[tuple]``\n - List of all possible solutions with corresponding probabilities. - The solution of the problem we are looking for should generally be the maximum probability. + qaoa_result : ``dict[str, float]``\n + Mapping of all variable assignments to their probabilities. Each + bit string has one bit per declared variable, from x0 on the left, + including variables absent from the objective after cancellation. + The highest-probability assignment is a heuristic candidate, not an + optimality certificate. Examples An example for minimization of quadratic binary function = -0.5 * x0 * x1 - 0.7 * x0 * x1 + 0.9 * x1 * x2 + 1.3 * x0 - x1 - 0.5 * x2 @@ -531,6 +534,9 @@ def run(self, layer=None, optimizer='SLSQP', optimizer_option=None): H = H_linear + H_quadratic + H_constant qaoa_model = qaoa.QAOA(problem=H) + # The operator may omit variables with zero or cancelled coefficients. + # Preserve the declared QUBO width and the original output bit positions. + qaoa_model.problem_dimension = n_key qaoa_result = qaoa_model.run(layer=layer, loss_type='default', optimize_type='default', optimizer=optimizer, optimizer_option=optimizer_option)[0] return qaoa_result diff --git a/test/QAlgBase/Test_QUBO_QAOA_register.py b/test/QAlgBase/Test_QUBO_QAOA_register.py new file mode 100644 index 00000000..d9212026 --- /dev/null +++ b/test/QAlgBase/Test_QUBO_QAOA_register.py @@ -0,0 +1,66 @@ +"""Preserve declared QUBO variables, including zero-coefficient variables.""" + +import numpy as np +import pytest +from pyqpanda_alg.QUBO import QUBO_QAOA + + +@pytest.mark.parametrize( + "linear, quadratic", + [ + ([0, 2], None), + ([2, 0], None), + ([0, 2, 0], None), + ([0, 0, 2], None), + ([0, 0, 0], None), + ([0, 0, 0], [[0, 0, 0], [0, 0, 3], [0, 0, 0]]), + ([0, -2], [[0, 0], [0, 2]]), + ([1, 2, 3], None), + ], + ids=[ + "unused-first-variable", + "unused-last-variable", + "unused-both-ends", + "only-last-variable-active", + "constant-objective", + "quadratic-only-with-unused-first", + "cancelled-coefficients", + "dense-control", + ], +) +def test_declared_variable_width_and_full_distribution( + linear: list[int], quadratic: list[list[int]] | None +) -> None: + """Compare actual CPU output with an independent dense one-layer evolution.""" + width = len(linear) + constant = 3 + matrix = np.zeros((width, width)) if quadratic is None else np.asarray(quadratic) + gamma, beta = np.random.RandomState(7).random(2) * np.pi + states = [format(key, f"0{width}b") for key in range(2**width)] + costs = [] + for bits in states: + variables = np.array([int(bit) for bit in bits]) + costs.append(variables @ matrix @ variables + variables @ linear + constant) + state = np.exp(-1j * gamma * np.asarray(costs)) / np.sqrt(2**width) + rotation = np.array( + [[np.cos(beta), 1j * np.sin(beta)], [1j * np.sin(beta), np.cos(beta)]] + ) + mixer = np.array([[1.0 + 0j]]) + for _ in range(width): + mixer = np.kron(mixer, rotation) + expected = np.abs(mixer @ state) ** 2 + + model = QUBO_QAOA({"quadratic": quadratic, "linear": linear, "constant": constant}) + random_state = np.random.get_state() + try: + np.random.seed(7) + # Zero optimizer iterations evaluate the seeded initial parameters and + # still run the real cost circuit, mixer, CPUQVM and result decoding. + result = model.run(layer=1, optimizer_option={"options": {"maxiter": 0}}) + finally: + np.random.set_state(random_state) + + assert set(result) == set(states), "Every declared variable needs an output bit" + actual = np.array([result[bits] for bits in states]) + np.testing.assert_allclose(actual, expected, atol=1e-12, rtol=0) + assert sum(result.values()) == pytest.approx(1.0)