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
62 changes: 47 additions & 15 deletions pyqpanda-algorithm/pyqpanda_alg/Grover/Grover_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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


Expand Down
4 changes: 3 additions & 1 deletion pyqpanda-algorithm/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ mypy>=1.14
requests
pycryptodome
sphinx-autoapi
pyqpanda3
pyqpanda3
pandas
scikit-learn
57 changes: 57 additions & 0 deletions test/QAlgBase/Test_grover_iter_num.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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__":
# 直接运行所有测试
Expand Down
115 changes: 82 additions & 33 deletions test/QAlgBase/Test_grover_mark_data_reflection.py
Original file line number Diff line number Diff line change
@@ -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"])
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"])
2 changes: 1 addition & 1 deletion test/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
testpaths =
QAlgBase
QAOA
QRAM
QARM
QPCA
QSVM

Expand Down