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