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
29 changes: 20 additions & 9 deletions pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from math import ceil
from typing import Union, Optional, List

import numpy as np
Expand Down Expand Up @@ -104,6 +105,11 @@ def query_qnumber(self) -> List[int]:
Returns
[n_key, n_res] : ``list[int]``\n
Returns the size(number of qubits) of the variable and result registers for the given problem.
The result register includes a sign bit and has at least one
qubit, including for the zero polynomial. Its two's-complement
range covers conservative bounds from all coefficient signs.
Fractional bounds are rounded outward for allocation; this
does not make fractional phase encoding exact integer arithmetic.

Examples
An example for function = -0.5 * x0 * x1 - 0.7 * x0 * x1 + 0.9 * x1 * x2 + 1.3 * x0 - x1 - 0.5 * x2
Expand All @@ -125,18 +131,23 @@ def query_qnumber(self) -> List[int]:
def pos(x): return x > 0
def neg(x): return x < 0

# NumPy integer sums/abs can overflow before the bit-width calculation.
def scalar(x): return int(x) if isinstance(x, np.integer) else x

max_val = 0
max_val += sum(sum(q_ij for q_ij in q_i if pos(q_ij)) for q_i in self.quadratic)
max_val += sum(l_i for l_i in self.linear if pos(l_i))
max_val += self.constant if pos(self.constant) else 0
max_val += sum(sum(scalar(q_ij) for q_ij in q_i if pos(q_ij)) for q_i in self.quadratic)
max_val += sum(scalar(l_i) for l_i in self.linear if pos(l_i))
max_val += scalar(self.constant) if pos(self.constant) else 0

min_val = 0
min_val += sum(sum(q_ij for q_ij in q_i if neg(q_ij)) for q_i in self.quadratic)
min_val += sum(l_i for l_i in self.linear if neg(l_i))
min_val += self.constant if neg(self.constant) else 0

pos_bits = int(np.ceil(np.log2(max_val + 1))) if max_val > 0 else 0
neg_bits = int(np.ceil(np.log2(abs(min_val)))) + 1 if min_val < 0 else 0
min_val += sum(sum(scalar(q_ij) for q_ij in q_i if neg(q_ij)) for q_i in self.quadratic)
min_val += sum(scalar(l_i) for l_i in self.linear if neg(l_i))
min_val += scalar(self.constant) if neg(self.constant) else 0

# Signed range: -2**(m-1) <= value <= 2**(m-1)-1.
# Integer envelopes avoid logarithm rounding at power-of-two bounds.
pos_bits = ceil(max_val).bit_length() + 1
neg_bits = (max(1, ceil(abs(min_val))) - 1).bit_length() + 1
n_res = max(pos_bits, neg_bits)

return [n_key, n_res]
Expand Down
283 changes: 192 additions & 91 deletions test/QAlgBase/Test_QUBO_query_qnumber.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,192 @@

import pytest
import sympy as sp
import numpy as np
from pyqpanda_alg.QUBO import QUBO


class Test_QUBO_query_qnumber:

@staticmethod
def calculate_function_range_and_nres(function, variables):
quadratic, linear, constant = Test_QUBO_query_qnumber._quadratic_func_to_coeff(function, variables)

def pos(x): return x > 0

def neg(x): return x < 0

max_val = 0
max_val += sum(sum(q_ij for q_ij in q_i if pos(q_ij)) for q_i in quadratic)
max_val += sum(l_i for l_i in linear if pos(l_i))
max_val += constant if pos(constant) else 0

min_val = 0
min_val += sum(sum(q_ij for q_ij in q_i if neg(q_ij)) for q_i in quadratic)
min_val += sum(l_i for l_i in linear if neg(l_i))
min_val += constant if neg(constant) else 0

pos_bits = int(np.ceil(np.log2(max_val + 1))) if max_val > 0 else 0
neg_bits = int(np.ceil(np.log2(abs(min_val)))) + 1 if min_val < 0 else 0
n_res_rel = max(pos_bits, neg_bits)

return n_res_rel

@staticmethod
def _quadratic_func_to_coeff(quadratic_func, variables):
quadratic_func = sp.Poly(quadratic_func, variables)
monoms = quadratic_func.monoms()
coeffs = quadratic_func.coeffs()

n = len(variables)
constant = 0
linear = np.zeros(n)
quadratic = np.zeros((n, n))

for i, monom in enumerate(monoms):
coeff = coeffs[i]
# 计算单项式中每个变量的指数
exp_dict = dict(zip(variables, monom))

# 统计非零指数的变量
nonzero_vars = [var for var in variables if exp_dict.get(var, 0) > 0]
nonzero_exps = [exp_dict[var] for var in nonzero_vars]

if len(nonzero_vars) == 0:
# 常数项
constant = coeff
elif len(nonzero_vars) == 1 and nonzero_exps[0] == 1:
# 线性项
var_index = variables.index(nonzero_vars[0])
linear[var_index] += coeff
elif len(nonzero_vars) == 1 and nonzero_exps[0] == 2:
# 二次项 (x_i^2 = x_i for binary variables)
var_index = variables.index(nonzero_vars[0])
linear[var_index] += coeff # x_i^2 = x_i
elif len(nonzero_vars) == 2 and all(exp == 1 for exp in nonzero_exps):
# 交叉二次项
var1_index = variables.index(nonzero_vars[0])
var2_index = variables.index(nonzero_vars[1])
quadratic[var1_index][var2_index] += coeff
else:
# 处理其他情况(理论上不应该出现,因为这是二次函数)
raise ValueError(f"不支持的单项式: {monom}")

return quadratic, linear, constant

def test_query_qnumber_basic_function(self):

x0, x1, x2 = sp.symbols('x0 x1 x2')
function = -0.5 * x0 * x1 - 0.7 * x0 * x1 + 0.9 * x1 * x2 + 1.3 * x0 - x1 - 0.5 * x2
test0 = QUBO.QuadraticBinary(function)
n_key, n_res = test0.query_qnumber()
n_res_rel = Test_QUBO_query_qnumber.calculate_function_range_and_nres(function, [x0, x1, x2])
assert n_key == 3, f"对于3个变量的问题,n_key应该为3,实际是{n_key}"
assert n_res > 0, f"n_res应该大于0,实际是{n_res}"
assert n_res == n_res_rel, f"n_res计算错误: 实际值{n_res}, 理论值{n_res_rel}"



if __name__ == "__main__":
# 运行测试
pytest.main([__file__, "-v", "-s"])
"""Check signed QUBO widths by decoding CPU arithmetic and phase oracles."""

from itertools import product

import numpy as np
import pytest
import sympy as sp
from pyqpanda3.core import CPUQVM, H, QProg, X
from pyqpanda_alg.QUBO.QUBO import QuadraticBinary, QUBO_GAS_origin


def _linear(coefficient: int | float) -> dict:
"""Build an explicit one-variable input, including the zero polynomial."""
return {"quadratic": [[0]], "linear": [coefficient], "constant": 0}


def _value(data: dict, bits: tuple[int, ...]) -> int | float:
"""Compute the original polynomial independently of QuadraticBinary."""
return (
data["constant"]
+ sum(a * x for a, x in zip(data["linear"], bits, strict=True))
+ sum(
data["quadratic"][i][j] * bits[i] * bits[j]
for i in range(len(bits))
for j in range(len(bits))
)
)


def _assert_encoded_values(data: dict) -> None:
"""Verify every basis input has the correct deterministic signed result."""
problem = QuadraticBinary(data)
key_width, value_width = map(int, problem.query_qnumber())
assert value_width >= 1
for bits in product((0, 1), repeat=key_width):
value = int(_value(data, bits))
assert -(1 << (value_width - 1)) <= value < (1 << (value_width - 1))
program = QProg(key_width + value_width)
for qubit, bit in enumerate(bits):
if bit:
program << X(qubit)
program << problem.cir(
list(range(key_width)), list(range(key_width, key_width + value_width))
)
machine = CPUQVM()
machine.run(program, shots=1)
state = np.asarray(machine.result().get_state_vector())
key = sum(bit << i for i, bit in enumerate(bits))
encoded = value % (1 << value_width)
target = key | (encoded << key_width)
assert abs(state[target]) ** 2 == pytest.approx(1.0, abs=1e-12)
decoded = (
encoded - (1 << value_width) if encoded >> (value_width - 1) else encoded
)
assert decoded == value


@pytest.mark.parametrize(
("coefficient", "expected_width"),
[
(0, 1),
(1, 2),
(2, 3),
(3, 3),
(4, 4),
(7, 4),
(-1, 1),
(-2, 2),
(-3, 3),
(-4, 3),
],
)
def test_integer_boundaries_encode_with_a_sign_bit(
coefficient: int, expected_width: int
) -> None:
"""Exercise zero, both signs, powers of two and their adjacent values."""
data = _linear(coefficient)
assert QuadraticBinary(data).query_qnumber() == [1, expected_width]
_assert_encoded_values(data)


@pytest.mark.parametrize(
("coefficient", "expected_width"),
[
(0.25, 2),
(-0.25, 1),
(1.25, 3),
(-2.25, 3),
(2**53 - 1, 54),
(2**53, 55),
(2**53 + 1, 55),
(-(2**53), 54),
(-(2**53) - 1, 55),
],
)
def test_fractional_envelopes_and_large_integer_widths(
coefficient: int | float, expected_width: int
) -> None:
"""Check allocation only, without fractional simulation or 55-qubit claims."""
assert QuadraticBinary(_linear(coefficient)).query_qnumber() == [1, expected_width]


@pytest.mark.parametrize(
"data",
[
{"quadratic": [[0, 3], [0, 0]], "linear": [0, 0], "constant": 0},
{"quadratic": [[1, 2], [-1, 0]], "linear": [-3, 4], "constant": 1},
{"quadratic": [[0]], "linear": [0], "constant": 3},
],
)
def test_quadratic_diagonal_cross_terms_and_constant_encode(data: dict) -> None:
"""Cover the full supported matrix form, not only linear expressions."""
_assert_encoded_values(data)


@pytest.mark.parametrize(
"data",
[
_linear(0),
_linear(4),
{"quadratic": [[0, 2], [1, 0]], "linear": [3, -2], "constant": -1},
],
)
@pytest.mark.parametrize("threshold", [0, 1, 5])
def test_gas_oracle_marks_only_values_below_threshold(
data: dict, threshold: int
) -> None:
"""Compare coherent signs and ancilla uncomputation to the classical predicate."""
problem = QUBO_GAS_origin(data)
key_width = len(data["linear"])
value_width = int(problem._n_value_function(threshold))
assert value_width >= 1
program = QProg(key_width + value_width)
for qubit in range(key_width):
program << H(qubit)
program << problem._flip_oracle_function(
list(range(key_width + value_width)), threshold
)
machine = CPUQVM()
machine.run(program, shots=1)
actual = np.asarray(machine.result().get_state_vector())
expected = np.zeros(1 << (key_width + value_width), dtype=complex)
for key in range(1 << key_width):
bits = tuple((key >> i) & 1 for i in range(key_width))
expected[key] = (-1 if _value(data, bits) < threshold else 1) / np.sqrt(
1 << key_width
)
np.testing.assert_allclose(actual, expected, atol=1e-12, rtol=0)


def test_original_fractional_example_retains_its_width() -> None:
"""Preserve the documented example without mirroring the allocation formula."""
x0, x1, x2 = sp.symbols("x0 x1 x2")
expression = -1.2 * x0 * x1 + 0.9 * x1 * x2 + 1.3 * x0 - x1 - 0.5 * x2
assert QuadraticBinary(expression).query_qnumber() == [3, 3]


@pytest.mark.parametrize(
"quadratic, linear, constant, expected_width",
[
(None, np.array([-(2**63)], dtype=np.int64), 0, 64),
(None, np.array([2**62] * 3, dtype=np.int64), 0, 65),
(None, np.array([2**61] * 8, dtype=np.int64), 0, 66),
(None, np.array([2**63] * 2, dtype=np.uint64), 0, 66),
(None, np.array([2**30] * 4, dtype=np.int32), 0, 34),
(None, [0], np.int64(2**63 - 1), 64),
(None, [0], np.uint64(2**64 - 1), 65),
(np.diag(np.array([2**62] * 3, dtype=np.int64)), None, 0, 65),
],
ids=[
"int64-minimum-absolute-value",
"int64-positive-wraparound",
"int64-wraparound-to-zero",
"uint64-wraparound",
"int32-wraparound",
"int64-constant-exact-boundary",
"uint64-constant-exact-boundary",
"quadratic-int64-wraparound",
],
)
def test_numpy_integer_bounds_do_not_overflow(
quadratic: np.ndarray | None,
linear: np.ndarray | list[int] | None,
constant: int | np.integer,
expected_width: int,
) -> None:
"""Check allocation only: these widths must never trigger a huge simulation."""
problem = QuadraticBinary(
{"quadratic": quadratic, "linear": linear, "constant": constant}
)
with np.errstate(over="raise"):
assert problem.query_qnumber()[1] == expected_width