diff --git a/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py b/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py index 576f1034..a155b8d8 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py +++ b/pyqpanda-algorithm/pyqpanda_alg/QUBO/QUBO.py @@ -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 @@ -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 @@ -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] diff --git a/test/QAlgBase/Test_QUBO_query_qnumber.py b/test/QAlgBase/Test_QUBO_query_qnumber.py index 4e3b8628..7f4eda2d 100644 --- a/test/QAlgBase/Test_QUBO_query_qnumber.py +++ b/test/QAlgBase/Test_QUBO_query_qnumber.py @@ -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"]) \ No newline at end of file +"""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