From e1992fb064accb6e564f65f4b10f351e6a5c2a2d Mon Sep 17 00:00:00 2001 From: CREVIOS <48938983+CREVIOS@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:50:41 +0600 Subject: [PATCH] Grover: use exact optimal iteration count and validate inputs Grover.iter_num used floor(pi/4 * sqrt(N/M)), which is the small-angle limit of the exact optimum and only holds while M << N. The success probability after k iterations is sin^2((2k+1)*theta/2) with theta = 2*arcsin(sqrt(M/N)), so the first maximum is at k* = pi/(2*theta) - 1/2 rounded to the nearest integer. Sweeping every (q_num, sol_num) with q_num <= 14 gives 4154 cases where the previous formula returned a strictly worse iteration count and no case where it was better. The largest gap is q_num=13, sol_num=5053, where the old value k=1 succeeds with probability 0.175 while the optimum k=0 succeeds with probability 0.617. The single-solution results that the small-angle limit already got right are unchanged, including the value shown in the docstring example. iter_num now also rejects sol_num outside [1, 2**q_num]; previously sol_num=0 raised ZeroDivisionError. mark_data_reflection silently marked the wrong state when a mark_data entry was longer than the qubit register, because only its lowest bits were consumed, and raised an opaque IndexError when it was shorter. It now validates the length and alphabet of every entry. It also emitted a BARRIER on each '1' position. Those do not change the state but block the transpiler from merging neighbouring gates; for a 6-qubit, 4-target reflection they cost 41 extra gates and 8 extra layers after transpilation at optimization level 2. They are removed, and a new test asserts the operator stays diagonal with -1 exactly on the marked states. Additionally: - requirements.txt: add pandas and scikit-learn. QSVD.py and QSVR.py import them at module scope, so `import pyqpanda_alg` failed with ModuleNotFoundError on a clean install. - test/pytest.ini: testpaths listed QRAM, which does not exist, while the existing QARM tests were never collected. - Re-enable Test_grover_mark_data_reflection.py, which was commented out in full but passes against the current API. --- .../pyqpanda_alg/Grover/Grover_core.py | 62 +++++++--- pyqpanda-algorithm/requirements.txt | 4 +- test/QAlgBase/Test_grover_iter_num.py | 57 +++++++++ .../Test_grover_mark_data_reflection.py | 115 +++++++++++++----- test/pytest.ini | 2 +- 5 files changed, 190 insertions(+), 50 deletions(-) diff --git a/pyqpanda-algorithm/pyqpanda_alg/Grover/Grover_core.py b/pyqpanda-algorithm/pyqpanda_alg/Grover/Grover_core.py index 0d2c2b3c..97672413 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/Grover/Grover_core.py +++ b/pyqpanda-algorithm/pyqpanda_alg/Grover/Grover_core.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pyqpanda3.core import CPUQVM, QCircuit, QProg, Z, X, H, BARRIER +from pyqpanda3.core import CPUQVM, QCircuit, QProg, Z, X, H import numpy as np from .. plugin import * @@ -143,15 +143,29 @@ def iter_num(q_num, sol_num): """ Calculate the optimal number of iterations in Grover search. + After :math:`k` iterations the success probability is + :math:`\\sin ^ 2 ((2 k + 1) \\theta / 2)` with + :math:`\\theta = 2 \\arcsin \\sqrt{M / N}`, so the first maximum is reached at + :math:`k ^ * = \\pi / (2 \\theta) - 1 / 2`, rounded to the nearest integer. + + The widely quoted closed form :math:`\\lfloor \\pi \\sqrt{N / M} / 4 \\rfloor` + is the small-angle limit of that expression and is only accurate while + :math:`M \\ll N`. It loses up to a full iteration when the solution set is a + sizeable fraction of the search space, so the exact form is used here. + Parameters q_num : ``int``\n The number of qubits in the search space. Search space size: :math:`N = 2 ^ {\\text {q_num}}`. sol_num : ``int``\n - Number of target solution states. + Number of target solution states. Must satisfy :math:`1 \\leq \\text{sol_num} \\leq N`. Returns num : The optimal number of iterations in Grover search. + Raises + ValueError\n + If ``q_num`` is negative, or ``sol_num`` is not in :math:`[1, N]`. + Examples An example for the case we show in the Grover search circuit. And we know there is only one solution to be found. And total 2 qubits for the search space. @@ -173,8 +187,16 @@ def iter_num(q_num, sol_num): best iter num: 1 """ - num = int(np.floor(np.pi * np.sqrt(2 ** q_num / sol_num) / 4)) - return num + if q_num < 0: + raise ValueError(f'q_num must be non-negative, got {q_num}') + + space_size = 2 ** q_num + if sol_num < 1 or sol_num > space_size: + raise ValueError(f'sol_num must be in [1, 2 ** q_num] = [1, {space_size}], got {sol_num}') + + theta = 2 * np.arcsin(np.sqrt(sol_num / space_size)) + num = int(np.round(np.pi / (2 * theta) - 0.5)) + return max(num, 0) def iter_analysis(q_num, sol_num, iternum=1): @@ -355,21 +377,31 @@ def mark_data_reflection(qubits: list = None, mark_data=None): if isinstance(mark_data, str): mark_data = [mark_data] + if mark_data is None or len(mark_data) == 0: + raise ValueError('mark_data must contain at least one target state') n = len(qubits) for i in mark_data: - for j in range(n): - if i[-(j + 1)] == '0': - flip_operator << X(qubits[j]) - else: - flip_operator << BARRIER([qubits[j]]) + # Without this check a string longer than the qubit register silently + # marks the state given by its lowest n bits, and a shorter one raises + # an opaque IndexError from the slicing below. + if len(i) != n: + raise ValueError(f'mark_data entry {i!r} has length {len(i)}, ' + f'but {n} qubits were given') + if any(bit not in '01' for bit in i): + raise ValueError(f'mark_data entry {i!r} must contain only the characters 0 and 1') + + # Only the '0' positions need an X conjugation; the '1' positions are + # already selected by the controls. The former implementation emitted a + # BARRIER on every '1' position, which is an identity on the state but + # blocks the transpiler from merging neighbouring gates. + zero_positions = [qubits[j] for j in range(n) if i[-(j + 1)] == '0'] + + for q in zero_positions: + flip_operator << X(q) flip_operator << Z(qubits[-1]).control(qubits[:-1]) - - for j in range(n): - if i[-(j + 1)] == '0': - flip_operator << X(qubits[j]) - else: - flip_operator << BARRIER([qubits[j]]) + for q in zero_positions: + flip_operator << X(q) return flip_operator diff --git a/pyqpanda-algorithm/requirements.txt b/pyqpanda-algorithm/requirements.txt index 94650a6e..ac9919dd 100644 --- a/pyqpanda-algorithm/requirements.txt +++ b/pyqpanda-algorithm/requirements.txt @@ -7,4 +7,6 @@ mypy>=1.14 requests pycryptodome sphinx-autoapi -pyqpanda3 \ No newline at end of file +pyqpanda3 +pandas +scikit-learn diff --git a/test/QAlgBase/Test_grover_iter_num.py b/test/QAlgBase/Test_grover_iter_num.py index 6c4b0942..db90709b 100644 --- a/test/QAlgBase/Test_grover_iter_num.py +++ b/test/QAlgBase/Test_grover_iter_num.py @@ -1,8 +1,15 @@ import pytest import math +import numpy as np from pyqpanda_alg.Grover import iter_num +def _success_probability(q_num, sol_num, iternum): + """给定迭代次数下 Grover 搜索的成功概率。""" + theta = 2 * math.asin(math.sqrt(sol_num / 2 ** q_num)) + return math.sin((2 * iternum + 1) * theta / 2) ** 2 + + class Test_grover_iter_num: def test_iter_num_basic_case(self): @@ -19,6 +26,56 @@ def test_iter_num_basic_case(self): theoretical_r = round((math.pi / 4) * math.sqrt(N / M)) assert abs(result - theoretical_r) <= 1, f"迭代次数 {result} 与理论值 {theoretical_r} 差异过大" + def test_iter_num_is_the_first_maximum(self): + """返回值应当正好是成功概率第一个极大值处的迭代次数。""" + for q_num in range(1, 11): + for sol_num in range(1, 2 ** q_num + 1): + result = iter_num(q_num=q_num, sol_num=sol_num) + theta = 2 * math.asin(math.sqrt(sol_num / 2 ** q_num)) + expected = max(0, int(round(math.pi / (2 * theta) - 0.5))) + assert result == expected, \ + f"q_num={q_num}, sol_num={sol_num}: 期望 {expected},实际 {result}" + + def test_iter_num_no_better_neighbour(self): + """相邻迭代次数的成功概率都不应优于返回值处的成功概率。""" + for q_num in range(1, 11): + for sol_num in range(1, 2 ** q_num): + best = iter_num(q_num=q_num, sol_num=sol_num) + p_best = _success_probability(q_num, sol_num, best) + for neighbour in (best - 1, best + 1): + if neighbour < 0: + continue + p_neighbour = _success_probability(q_num, sol_num, neighbour) + assert p_best >= p_neighbour - 1e-12, \ + (f"q_num={q_num}, sol_num={sol_num}: 迭代 {neighbour} 次的成功概率 " + f"{p_neighbour:.6f} 高于返回值 {best} 次的 {p_best:.6f}") + + def test_iter_num_dense_solution_set(self): + """解集较大时不应使用小角度近似,否则会多迭代一次并显著降低成功概率。""" + # N = 8192, M = 5053 时,小角度近似给出 1,而最优值为 0。 + q_num, sol_num = 13, 5053 + result = iter_num(q_num=q_num, sol_num=sol_num) + assert result == 0, f"稠密解集下最优迭代次数应为 0,实际为 {result}" + assert _success_probability(q_num, sol_num, 0) > \ + _success_probability(q_num, sol_num, 1), "0 次迭代的成功概率应高于 1 次" + + def test_iter_num_single_solution_matches_classic_formula(self): + """单解且解集稀疏时,应与经典公式 floor(pi/4 * sqrt(N)) 一致。""" + for q_num in range(4, 15): + expected = int(np.floor(np.pi / 4 * np.sqrt(2 ** q_num))) + assert iter_num(q_num=q_num, sol_num=1) == expected, \ + f"q_num={q_num} 时单解情形应与经典公式一致" + + @pytest.mark.parametrize("q_num, sol_num", [(3, 0), (3, -1), (3, 9), (0, 2)]) + def test_iter_num_rejects_invalid_solution_count(self, q_num, sol_num): + """解的数量超出 [1, 2 ** q_num] 时应抛出 ValueError 而不是返回错误结果。""" + with pytest.raises(ValueError): + iter_num(q_num=q_num, sol_num=sol_num) + + def test_iter_num_rejects_negative_qubit_number(self): + with pytest.raises(ValueError): + iter_num(q_num=-1, sol_num=1) + if __name__ == "__main__": # 直接运行所有测试 diff --git a/test/QAlgBase/Test_grover_mark_data_reflection.py b/test/QAlgBase/Test_grover_mark_data_reflection.py index a18cb5ad..58718866 100644 --- a/test/QAlgBase/Test_grover_mark_data_reflection.py +++ b/test/QAlgBase/Test_grover_mark_data_reflection.py @@ -1,33 +1,82 @@ -# import pytest -# from pyqpanda_alg.Grover import mark_data_reflection -# from pyqpanda_alg.Grover import Grover -# from pyqpanda3.core import CPUQVM, QProg -# -# -# class Test_grover_mark_data_reflection: -# -# def test_mark_data_reflection_basic(self): -# m = CPUQVM() -# q_state = QProg(3).qubits() -# -# def mark(qubits): -# return mark_data_reflection(qubits=qubits, mark_data=['101', '001']) -# -# demo_search = Grover(flip_operator=mark) -# prog = QProg() -# prog << demo_search.cir(q_input=q_state) -# -# m.run(prog, shots=1000) -# res = m.result().get_prob_dict() -# assert '101' in res, "目标态 '101' 应该在结果中" -# assert '001' in res, "目标态 '001' 应该在结果中" -# -# target_prob = res.get('101', 0) + res.get('001', 0) -# assert target_prob > 0.1, f"两个目标态的总概率应该显著高于随机,当前为: {target_prob}" -# -# total_prob = sum(res.values()) -# assert abs(total_prob - 1.0) < 0.01, f"概率总和应该为1,当前为: {total_prob}" -# -# if __name__ == "__main__": -# # 可以直接运行测试 -# pytest.main([__file__, "-v", "-s"]) \ No newline at end of file +import pytest +import numpy as np +from pyqpanda_alg.Grover import mark_data_reflection +from pyqpanda_alg.Grover import Grover +from pyqpanda3.core import CPUQVM, QProg + + +class Test_grover_mark_data_reflection: + + def test_mark_data_reflection_basic(self): + m = CPUQVM() + q_state = QProg(3).qubits() + + def mark(qubits): + return mark_data_reflection(qubits=qubits, mark_data=['101', '001']) + + demo_search = Grover(flip_operator=mark) + prog = QProg() + prog << demo_search.cir(q_input=q_state) + + m.run(prog, shots=1000) + res = m.result().get_prob_dict() + assert '101' in res, "目标态 '101' 应该在结果中" + assert '001' in res, "目标态 '001' 应该在结果中" + + target_prob = res.get('101', 0) + res.get('001', 0) + assert target_prob > 0.1, f"两个目标态的总概率应该显著高于随机,当前为: {target_prob}" + + total_prob = sum(res.values()) + assert abs(total_prob - 1.0) < 0.01, f"概率总和应该为1,当前为: {total_prob}" + + def test_mark_data_reflection_flips_only_marked_states(self): + """该算子应当是对角的,且只在被标记的态上取 -1。""" + qubits = list(range(3)) + mark_data = ['101', '001'] + matrix = np.asarray(mark_data_reflection(qubits=qubits, mark_data=mark_data).matrix()) + + off_diagonal = np.max(np.abs(matrix - np.diag(np.diag(matrix)))) + assert off_diagonal < 1e-9, f"相位翻转算子应为对角矩阵,非对角最大值为 {off_diagonal}" + + marked = {int(s, 2) for s in mark_data} + for index in range(matrix.shape[0]): + expected = -1.0 if index in marked else 1.0 + assert abs(matrix[index, index] - expected) < 1e-9, \ + f"基态 {index:03b} 的相位应为 {expected},实际为 {matrix[index, index]}" + + def test_mark_data_reflection_accepts_single_string(self): + """单个字符串与仅含该字符串的列表应当等价。""" + qubits = list(range(3)) + from_string = np.asarray(mark_data_reflection(qubits=qubits, mark_data='011').matrix()) + from_list = np.asarray(mark_data_reflection(qubits=qubits, mark_data=['011']).matrix()) + assert np.max(np.abs(from_string - from_list)) < 1e-9, "字符串与单元素列表应生成相同电路" + + def test_mark_data_reflection_rejects_length_mismatch(self): + """标记串长度与量子比特数不一致时应报错,而不是静默标记错误的态。""" + qubits = list(range(3)) + with pytest.raises(ValueError): + mark_data_reflection(qubits=qubits, mark_data=['1010']) + with pytest.raises(ValueError): + mark_data_reflection(qubits=qubits, mark_data=['10']) + + def test_mark_data_reflection_rejects_invalid_characters(self): + qubits = list(range(3)) + with pytest.raises(ValueError): + mark_data_reflection(qubits=qubits, mark_data=['1x1']) + + def test_mark_data_reflection_rejects_empty_mark_data(self): + qubits = list(range(3)) + with pytest.raises(ValueError): + mark_data_reflection(qubits=qubits, mark_data=[]) + + def test_mark_data_reflection_contains_no_barrier(self): + """算子中不应包含对态无影响、却会阻断编译优化的 BARRIER。""" + qubits = list(range(4)) + circuit = mark_data_reflection(qubits=qubits, mark_data=['1011', '0110']) + op_names = {str(name).upper() for name in circuit.count_ops()} + assert 'BARRIER' not in op_names, f"电路中不应包含 BARRIER,实际算子为 {op_names}" + + +if __name__ == "__main__": + # 可以直接运行测试 + pytest.main([__file__, "-v", "-s"]) diff --git a/test/pytest.ini b/test/pytest.ini index 2887f64b..1dd65567 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -2,7 +2,7 @@ testpaths = QAlgBase QAOA - QRAM + QARM QPCA QSVM