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
19 changes: 15 additions & 4 deletions pyqpanda-algorithm/pyqpanda_alg/QAOA/default_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,19 @@ def iswap(q1, q2, angle):
return xycir


def parity_partition_xy_mixer(qlist, beta):
def parity_partition_xy_mixer(qlist: list[int], beta: float) -> QCircuit:
"""
Quantum circuits to approximate a parity-partition XY mixer.

Empty and singleton domains return the identity, preserving Hamming weight.
Two qubits receive one XY interaction. Larger domains follow the supplied
qubit order around a ring: even-indexed pairs, then odd-indexed pairs.
An odd ring closes with a separate last-to-first interaction; an even
ring includes that edge in the second partition.
The existing angle convention is retained: each pair implements
exp(+i * beta * (XX + YY)). This partitioned product is generally not
the exact exponential of the sum of all ring interactions.

Parameters
qlist : ``list``\n
Qubits list.\n
Expand Down Expand Up @@ -104,10 +113,9 @@ def parity_partition_xy_mixer(qlist, beta):
q_num = len(qlist)
cir = QCircuit()

if q_num < 2:
return cir
beta = - 2 * beta
if q_num == 1:
# cir << pq.RX(qlist, beta)
cir << RX(qlist[0], beta)
if q_num == 2:
# cir << pq.iSWAP(qlist[0], qlist[1], beta)
cir << iswap(qlist[0], qlist[1], beta)
Expand All @@ -123,6 +131,9 @@ def parity_partition_xy_mixer(qlist, beta):
else:
# cir << pq.iSWAP(qlist[-1], qlist[0], beta)
cir << iswap(qlist[-1], qlist[0], beta)
if q_num % 2:
# An odd ring needs a third partition for its closing edge.
cir << iswap(qlist[-1], qlist[0], beta)

return cir

Expand Down
150 changes: 112 additions & 38 deletions test/QAOA/Test_parity_partition_xy_mixer.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,112 @@
# import sys
# from pathlib import Path
# import pytest
# import numpy as np
#
# sys.path.append((Path.cwd().parent.parent).__str__())
#
# from pyqpanda_alg.QAOA import default_circuits
# from pyqpanda3.core import QProg, RX, RY, RZ, CPUQVM
#
#
# class TestParityPartitionXYMixer:
#
# def setup_method(self):
# self.machine = CPUQVM()
#
# def test_parity_partition_xy_mixer_spin_conservation(self):
# for n_qubits in [2, 3, 4, 5]:
# prog = QProg(n_qubits)
# qubits = prog.qubits()
# beta = np.pi / 6
#
# for q in qubits:
# prog << RX(q, np.random.random() * 2 * np.pi)
#
# self.machine.run(prog, shots=1000)
# original_result = self.machine.result().get_prob_dict()
# print(original_result)
# circuit = default_circuits.parity_partition_xy_mixer(qubits, beta)
# prog << circuit
#
# self.machine.run(prog, shots=1000)
# final_result = self.machine.result().get_prob_dict()
# print(final_result)
#
#
# if __name__ == "__main__":
# pytest.main([__file__, "-v"])
"""Verify parity-ring mixers against independent dense XY Hamiltonians."""

import numpy as np
import pytest
from pyqpanda3.core import CPUQVM, QCircuit, QProg, X
from pyqpanda_alg.QAOA.default_circuits import (
parity_partition_xy_mixer,
xy_mixer,
)
from scipy.linalg import expm


def _unitary(circuit: QCircuit, width: int) -> np.ndarray:
"""Simulate every computational-basis column, including complex phases."""
columns = []
for basis in range(1 << width):
program = QProg(width)
for qubit in range(width):
if basis & (1 << qubit):
program << X(qubit)
program << circuit
machine = CPUQVM()
machine.run(program, shots=1)
columns.append(machine.result().get_state_vector())
return np.asarray(columns).T


def _reference(domain: list[int], width: int, beta: float) -> np.ndarray:
"""Exponentiate disjoint XY partitions; retain the existing angle sign."""
size = len(domain)
identity = np.eye(1 << width, dtype=complex)
if size < 2:
return identity
if size == 2:
partitions = [[(domain[0], domain[1])]]
else:
edges = [(domain[i], domain[(i + 1) % size]) for i in range(size)]
# For odd rings, the wrap edge is a separate third partition.
interior = edges[:-1] if size % 2 else edges
partitions = [interior[::2], interior[1::2]]
if size % 2:
partitions.append([edges[-1]])
result = identity
for partition in partitions:
hamiltonian = np.zeros_like(identity)
for left, right in partition:
for basis in range(1 << width):
if ((basis >> left) & 1) != ((basis >> right) & 1):
swapped = basis ^ (1 << left) ^ (1 << right)
# XX + YY exchanges |01> and |10> with amplitude 2.
hamiltonian[swapped, basis] += 2
result = expm(1j * beta * hamiltonian) @ result
return result


@pytest.mark.parametrize("width", range(1, 7))
@pytest.mark.parametrize("beta", [0.0, 0.37, -0.23])
def test_full_parity_ring_unitary(width: int, beta: float) -> None:
"""Cover singleton identity, one two-qubit edge, and odd/even rings."""
domain = list(range(width))
circuit = parity_partition_xy_mixer(domain, beta)
actual = _unitary(circuit, width)
np.testing.assert_allclose(actual, _reference(domain, width, beta), atol=1e-12)
for basis in range(1 << width):
outside = [i for i in range(1 << width) if i.bit_count() != basis.bit_count()]
assert np.sum(np.abs(actual[outside, basis]) ** 2) < 1e-24
edges = 0 if width == 1 else 1 if width == 2 else width
assert dict(circuit.count_ops()).get("CNOT", 0) == 2 * edges


@pytest.mark.parametrize("domain", [[4, 1, 3], [5, 0, 3, 1, 4]])
def test_permuted_qubits_and_spectators(domain: list[int]) -> None:
"""Use caller qubit order, leaving spectator qubits unchanged."""
actual = _unitary(parity_partition_xy_mixer(domain, 0.19), 6)
np.testing.assert_allclose(actual, _reference(domain, 6, 0.19), atol=1e-12)


@pytest.mark.parametrize(
("argument", "domains", "width"),
[
([[0], [1, 4], [2, 5, 3]], [[0], [1, 4], [2, 5, 3]], 6),
(3, [[0], [1], [2]], 3),
(2, [[0, 1, 2], [3, 4, 5]], 6),
],
)
def test_factory_preserves_each_domain(
argument: int | list[list[int]], domains: list[list[int]], width: int
) -> None:
"""Exercise public integer/list domain paths over all Hamming sectors."""
actual = _unitary(xy_mixer(argument, "PXY")(list(range(width)), 0.37), width)
expected = np.eye(1 << width, dtype=complex)
for domain in domains:
expected = _reference(domain, width, 0.37) @ expected
np.testing.assert_allclose(actual, expected, atol=1e-12)
for basis in range(1 << width):
outside = [
other
for other in range(1 << width)
if any(
sum((basis >> q) & 1 for q in domain)
!= sum((other >> q) & 1 for q in domain)
for domain in domains
)
]
assert np.sum(np.abs(actual[outside, basis]) ** 2) < 1e-24


def test_empty_domain_remains_a_no_op() -> None:
"""Preserve the previously accepted empty-circuit behavior."""
circuit = parity_partition_xy_mixer([], 0.37)
assert not dict(circuit.count_ops())
np.testing.assert_allclose(_unitary(circuit, 1), np.eye(2), atol=1e-12)