Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions pyqpanda-algorithm/pyqpanda_alg/QAOA/qaoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -318,17 +322,17 @@ 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
self.operator = problem
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")
Expand Down
12 changes: 9 additions & 3 deletions pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
144 changes: 102 additions & 42 deletions test/QAOA/Test_qaoa_QAOA_calculate_energy.py
Original file line number Diff line number Diff line change
@@ -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"])
"""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
66 changes: 66 additions & 0 deletions test/QAlgBase/Test_QUBO_QAOA_register.py
Original file line number Diff line number Diff line change
@@ -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)