From 2ccb54bee871465899a730a6915da46114b33757 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 13 Sep 2026 09:29:00 -0400 Subject: [PATCH 01/12] feat: add oracle-learning algorithms --- .../QOracleLearning/oracle_learning.py | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/oracle_learning.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/oracle_learning.py b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/oracle_learning.py new file mode 100644 index 00000000..b12c975f --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/oracle_learning.py @@ -0,0 +1,252 @@ +"""Deutsch-Jozsa and Bernstein-Vazirani oracle-learning algorithms. + +Bit-order convention +-------------------- +Secret strings are q0-first: ``"101"`` means q0=1, q1=0, q2=1. +Truth-table index ``x`` uses the same convention through ordinary integer bits, +i.e. q0 is the least-significant bit of ``x``. + +The pure helpers in this module have no PyQPanda dependency. Circuit-building +and execution helpers import ``pyqpanda3`` lazily. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import Any + + +def _normalise_bits(bits: str | Sequence[int], *, name: str) -> tuple[int, ...]: + if isinstance(bits, str): + if not bits: + raise ValueError(f"{name} must not be empty") + if any(ch not in "01" for ch in bits): + raise ValueError(f"{name} must contain only '0' and '1'") + return tuple(int(ch) for ch in bits) + try: + values = tuple(int(v) for v in bits) + except (TypeError, ValueError) as exc: + raise ValueError(f"{name} must be a bit sequence") from exc + if not values: + raise ValueError(f"{name} must not be empty") + if any(v not in (0, 1) for v in values): + raise ValueError(f"{name} must contain only 0 and 1") + return values + + +def validate_truth_table(table: Sequence[int]) -> tuple[int, ...]: + """Validate a Boolean truth table and return it as an immutable tuple.""" + try: + values = tuple(int(v) for v in table) + except (TypeError, ValueError) as exc: + raise ValueError("truth table must be a sequence of bits") from exc + if len(values) < 2 or len(values) & (len(values) - 1): + raise ValueError("truth table length must be a power of two >= 2") + if any(v not in (0, 1) for v in values): + raise ValueError("truth table entries must be 0 or 1") + return values + + +def input_qubit_count(table: Sequence[int]) -> int: + values = validate_truth_table(table) + return (len(values) - 1).bit_length() + + +def index_to_q0_bits(index: int, width: int) -> tuple[int, ...]: + if not isinstance(width, int) or width <= 0: + raise ValueError("width must be a positive integer") + if not isinstance(index, int) or index < 0 or index >= 2**width: + raise ValueError("index does not fit the requested width") + return tuple((index >> qubit) & 1 for qubit in range(width)) + + +def q0_bits_to_index(bits: str | Sequence[int]) -> int: + values = _normalise_bits(bits, name="bits") + return sum(bit << qubit for qubit, bit in enumerate(values)) + + +def affine_truth_table(secret: str | Sequence[int], bias: int = 0) -> tuple[int, ...]: + """Return ``f(x) = secret·x XOR bias`` in q0-first convention.""" + secret_bits = _normalise_bits(secret, name="secret") + if bias not in (0, 1, False, True): + raise ValueError("bias must be 0 or 1") + b = int(bias) + table = [] + for x in range(2 ** len(secret_bits)): + parity = b + for qubit, coefficient in enumerate(secret_bits): + if coefficient: + parity ^= (x >> qubit) & 1 + table.append(parity) + return tuple(table) + + +def classify_deutsch_jozsa_promise(table: Sequence[int]) -> str: + """Classify a promised oracle as ``constant`` or ``balanced``.""" + values = validate_truth_table(table) + ones = sum(values) + if ones in (0, len(values)): + return "constant" + if ones * 2 == len(values): + return "balanced" + raise ValueError("Deutsch-Jozsa requires a constant or balanced truth table") + + +def phase_oracle_diagonal(table: Sequence[int]) -> tuple[int, ...]: + """Return the exact diagonal of the phase oracle ``(-1)**f(x)``.""" + return tuple(1 if bit == 0 else -1 for bit in validate_truth_table(table)) + + +def walsh_probabilities(table: Sequence[int]) -> tuple[float, ...]: + """Return exact output probabilities by independent Walsh transform.""" + values = validate_truth_table(table) + size = len(values) + probs: list[float] = [] + for y in range(size): + total = 0 + for x, fx in enumerate(values): + parity = (x & y).bit_count() & 1 + total += -1 if (fx ^ parity) else 1 + amplitude = total / size + probs.append(float(amplitude * amplitude)) + return tuple(probs) + + +def recover_affine_secret(table: Sequence[int]) -> str: + """Recover a q0-first BV secret, rejecting non-affine tables.""" + values = validate_truth_table(table) + n = input_qubit_count(values) + bias = values[0] + secret = tuple(values[1 << qubit] ^ bias for qubit in range(n)) + if values != affine_truth_table(secret, bias): + raise ValueError("truth table is not affine and is invalid for Bernstein-Vazirani") + return "".join(str(bit) for bit in secret) + + +def dominant_q0_bitstring(probabilities: Sequence[float], width: int) -> str: + """Decode a unique dominant probability-list entry as q0-first bits.""" + if len(probabilities) != 2**width: + raise ValueError("probability list length does not match width") + if any(p < -1e-12 for p in probabilities): + raise ValueError("probabilities must be non-negative") + maximum = max(probabilities) + winners = [i for i, p in enumerate(probabilities) if abs(p - maximum) <= 1e-12] + if len(winners) != 1: + raise ValueError("probability distribution has no unique dominant state") + return "".join(str(bit) for bit in index_to_q0_bits(winners[0], width)) + + +def _resolve_qubits(width: int, qubits: Iterable[Any] | None) -> list[Any]: + resolved = list(range(width)) if qubits is None else list(qubits) + if len(resolved) != width: + raise ValueError(f"expected exactly {width} qubits") + if len({str(q) for q in resolved}) != width: + raise ValueError("qubits must be distinct") + return resolved + + +def truth_table_phase_oracle(table: Sequence[int], qubits: Iterable[Any] | None = None): + """Build a direct PyQPanda3 phase oracle for an explicit truth table. + + Each marked basis state is surrounded by X gates for zero controls and gets + one n-qubit controlled-Z phase flip. This generic teaching synthesis is + worst-case O(2^n), not a scalability claim. + """ + values = validate_truth_table(table) + n = input_qubit_count(values) + q = _resolve_qubits(n, qubits) + from pyqpanda3.core import QCircuit, X, Z + + circuit = QCircuit() + for x, fx in enumerate(values): + if not fx: + continue + zero_qubits = [q[i] for i in range(n) if ((x >> i) & 1) == 0] + for qb in zero_qubits: + circuit << X(qb) + if n == 1: + circuit << Z(q[0]) + else: + circuit << Z(q[-1]).control(q[:-1]) + for qb in reversed(zero_qubits): + circuit << X(qb) + return circuit + + +def affine_phase_oracle(secret: str | Sequence[int], qubits: Iterable[Any] | None = None): + """Build the O(n) phase oracle for ``f(x)=secret·x XOR bias``. + + The affine bias is a global phase and therefore intentionally absent. + """ + secret_bits = _normalise_bits(secret, name="secret") + q = _resolve_qubits(len(secret_bits), qubits) + from pyqpanda3.core import QCircuit, Z + + circuit = QCircuit() + for coefficient, qb in zip(secret_bits, q): + if coefficient: + circuit << Z(qb) + return circuit + + +def deutsch_jozsa_circuit(table: Sequence[int], qubits: Iterable[Any] | None = None): + """Construct the ancilla-free phase-oracle Deutsch-Jozsa circuit.""" + values = validate_truth_table(table) + classify_deutsch_jozsa_promise(values) + n = input_qubit_count(values) + q = _resolve_qubits(n, qubits) + from pyqpanda3.core import H, QCircuit + + circuit = QCircuit() + for qb in q: + circuit << H(qb) + circuit << truth_table_phase_oracle(values, q) + for qb in q: + circuit << H(qb) + return circuit + + +def bernstein_vazirani_circuit(secret: str | Sequence[int], qubits: Iterable[Any] | None = None): + """Construct the ancilla-free Bernstein-Vazirani circuit in O(n) gates.""" + secret_bits = _normalise_bits(secret, name="secret") + q = _resolve_qubits(len(secret_bits), qubits) + from pyqpanda3.core import H, QCircuit + + circuit = QCircuit() + for qb in q: + circuit << H(qb) + circuit << affine_phase_oracle(secret_bits, q) + for qb in q: + circuit << H(qb) + return circuit + + +def _run_probabilities(circuit: Any, qubits: Sequence[Any], shots: int = 1024) -> tuple[float, ...]: + if not isinstance(shots, int) or shots <= 0: + raise ValueError("shots must be a positive integer") + from pyqpanda3.core import CPUQVM, QProg + + machine = CPUQVM() + program = QProg() + program << circuit + machine.run(program, shots) + return tuple(float(p) for p in machine.result().get_prob_list(list(qubits))) + + +def run_deutsch_jozsa(table: Sequence[int], shots: int = 1024) -> tuple[str, tuple[float, ...]]: + """Execute Deutsch-Jozsa and return ``(classification, probabilities)``.""" + values = validate_truth_table(table) + classify_deutsch_jozsa_promise(values) + n = input_qubit_count(values) + qubits = list(range(n)) + probs = _run_probabilities(deutsch_jozsa_circuit(values, qubits), qubits, shots) + classification = "constant" if probs[0] > 0.5 else "balanced" + return classification, probs + + +def run_bernstein_vazirani(secret: str | Sequence[int], shots: int = 1024) -> tuple[str, tuple[float, ...]]: + """Execute Bernstein-Vazirani and return q0-first secret + probabilities.""" + secret_bits = _normalise_bits(secret, name="secret") + qubits = list(range(len(secret_bits))) + probs = _run_probabilities(bernstein_vazirani_circuit(secret_bits, qubits), qubits, shots) + return dominant_q0_bitstring(probs, len(secret_bits)), probs From cadc68db74720c0030c8803ec488cd436d7eaa6a Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 13 Sep 2026 09:29:07 -0400 Subject: [PATCH 02/12] feat: export oracle-learning API --- .../pyqpanda_alg/QOracleLearning/__init__.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/__init__.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/__init__.py new file mode 100644 index 00000000..377344d3 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/__init__.py @@ -0,0 +1,39 @@ +"""Oracle-learning algorithms: Deutsch-Jozsa and Bernstein-Vazirani.""" + +from .oracle_learning import ( + affine_phase_oracle, + affine_truth_table, + bernstein_vazirani_circuit, + classify_deutsch_jozsa_promise, + deutsch_jozsa_circuit, + dominant_q0_bitstring, + index_to_q0_bits, + input_qubit_count, + phase_oracle_diagonal, + q0_bits_to_index, + recover_affine_secret, + run_bernstein_vazirani, + run_deutsch_jozsa, + truth_table_phase_oracle, + validate_truth_table, + walsh_probabilities, +) + +__all__ = [ + "affine_phase_oracle", + "affine_truth_table", + "bernstein_vazirani_circuit", + "classify_deutsch_jozsa_promise", + "deutsch_jozsa_circuit", + "dominant_q0_bitstring", + "index_to_q0_bits", + "input_qubit_count", + "phase_oracle_diagonal", + "q0_bits_to_index", + "recover_affine_secret", + "run_bernstein_vazirani", + "run_deutsch_jozsa", + "truth_table_phase_oracle", + "validate_truth_table", + "walsh_probabilities", +] From c9ec7eb05cf7bc586096c2d7749f398c3b592bf0 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 13 Sep 2026 09:29:25 -0400 Subject: [PATCH 03/12] test: validate oracle-learning algorithms --- test/QOracleLearning/Test_oracle_learning.py | 86 ++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/QOracleLearning/Test_oracle_learning.py diff --git a/test/QOracleLearning/Test_oracle_learning.py b/test/QOracleLearning/Test_oracle_learning.py new file mode 100644 index 00000000..576106c1 --- /dev/null +++ b/test/QOracleLearning/Test_oracle_learning.py @@ -0,0 +1,86 @@ +import pytest + +from pyqpanda_alg import QOracleLearning as q + + +def test_truth_table_validation_and_promise(): + assert q.classify_deutsch_jozsa_promise([0, 0, 0, 0]) == "constant" + assert q.classify_deutsch_jozsa_promise([1, 1, 1, 1]) == "constant" + assert q.classify_deutsch_jozsa_promise([0, 1, 1, 0]) == "balanced" + with pytest.raises(ValueError, match="constant or balanced"): + q.classify_deutsch_jozsa_promise([0, 0, 0, 1]) + for bad in ([0], [0, 1, 0], [0, 2], []): + with pytest.raises(ValueError): + q.validate_truth_table(bad) + + +def test_bit_order_round_trip_exhaustive(): + for width in range(1, 7): + for index in range(2**width): + bits = q.index_to_q0_bits(index, width) + assert q.q0_bits_to_index(bits) == index + + +def test_phase_oracle_diagonal_matches_truth_table(): + table = (0, 1, 1, 0, 1, 0, 0, 1) + assert q.phase_oracle_diagonal(table) == (1, -1, -1, 1, -1, 1, 1, -1) + + +def test_deutsch_jozsa_walsh_reference(): + for table in ((0,) * 8, (1,) * 8): + probs = q.walsh_probabilities(table) + assert probs[0] == pytest.approx(1.0) + assert sum(probs[1:]) == pytest.approx(0.0) + balanced = q.affine_truth_table("101", bias=0) + probs = q.walsh_probabilities(balanced) + assert probs[0] == pytest.approx(0.0) + assert sum(probs) == pytest.approx(1.0) + + +def test_all_bv_secrets_through_five_qubits_both_biases(): + for width in range(1, 6): + for secret_index in range(2**width): + secret = "".join(str(bit) for bit in q.index_to_q0_bits(secret_index, width)) + for bias in (0, 1): + table = q.affine_truth_table(secret, bias) + assert q.recover_affine_secret(table) == secret + probs = q.walsh_probabilities(table) + assert probs[secret_index] == pytest.approx(1.0) + assert sum(probs) == pytest.approx(1.0) + assert q.dominant_q0_bitstring(probs, width) == secret + + +def test_recover_secret_rejects_non_affine_table(): + with pytest.raises(ValueError, match="not affine"): + q.recover_affine_secret((0, 1, 1, 1)) + + +def test_secret_and_probability_validation(): + for secret in ("", "10x", [1, 2]): + with pytest.raises(ValueError): + q.affine_truth_table(secret) + with pytest.raises(ValueError): + q.affine_truth_table("10", bias=2) + with pytest.raises(ValueError): + q.dominant_q0_bitstring([0.5, 0.5], 1) + + +def test_cpuqvm_bv_end_to_end(): + for secret in ("0", "1", "1011", "01101"): + recovered, probs = q.run_bernstein_vazirani(secret) + assert recovered == secret + assert max(probs) == pytest.approx(1.0, abs=1e-9) + + +def test_cpuqvm_deutsch_jozsa_end_to_end(): + for table, expected in ( + ((0,) * 8, "constant"), + ((1,) * 8, "constant"), + (q.affine_truth_table("101"), "balanced"), + ): + result, probs = q.run_deutsch_jozsa(table) + assert result == expected + if expected == "constant": + assert probs[0] == pytest.approx(1.0, abs=1e-9) + else: + assert probs[0] == pytest.approx(0.0, abs=1e-9) From 12d40a092b5d964ba97ab685d0a614062da89c4e Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 13 Sep 2026 09:29:36 -0400 Subject: [PATCH 04/12] docs: document oracle-learning module --- .../pyqpanda_alg/QOracleLearning/README.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/README.md diff --git a/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/README.md b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/README.md new file mode 100644 index 00000000..7481292a --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QOracleLearning/README.md @@ -0,0 +1,35 @@ +# QOracleLearning + +`QOracleLearning` adds two textbook oracle-learning algorithms to `pyqpanda-algorithm`: + +- **Deutsch-Jozsa**: one quantum oracle query distinguishes a promised constant Boolean function from a balanced one. +- **Bernstein-Vazirani**: one quantum oracle query recovers the complete hidden affine bit string `s` in `f(x)=s·x XOR b`. + +## Bit-order contract + +All public secret strings are **q0-first**. `"101"` means `q0=1`, `q1=0`, `q2=1`. Truth-table index `x` uses ordinary integer bits, so q0 is the least-significant bit. Execution uses `get_prob_list()` and decodes the winning integer basis index explicitly, avoiding display-string endianness ambiguity. + +## Oracle construction + +`truth_table_phase_oracle()` accepts an explicit truth table and directly synthesizes the diagonal phase oracle `(-1)^f(x)`. This generic teaching path is worst-case `O(2^n)` gates and is not claimed to be scalable. + +`affine_phase_oracle()` uses the structure of Bernstein-Vazirani: each `s_i=1` contributes one Z gate, so the oracle is `O(n)`. The affine bias is a global phase and is intentionally omitted. + +## Example + +```python +from pyqpanda_alg import QOracleLearning + +secret, probs = QOracleLearning.run_bernstein_vazirani("1011") +assert secret == "1011" + +oracle = QOracleLearning.affine_truth_table("101") +kind, probs = QOracleLearning.run_deutsch_jozsa(oracle) +assert kind == "balanced" +``` + +## Independent correctness reference + +`walsh_probabilities()` computes the exact classical Walsh-Hadamard spectrum. It is deliberately independent of PyQPanda3 and is used to test every hidden string through five qubits under both affine biases. The test suite also covers promise rejection, non-affine rejection, bit-order round trips, phase-oracle signs, and CPUQVM end-to-end execution. + +The module imports PyQPanda3 lazily: pure truth-table / reference helpers remain usable for validation and documentation tooling without initializing a quantum backend. From b04476c0e4486d8b6e3bc503965d8fd1dac9192d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 13 Sep 2026 09:29:44 -0400 Subject: [PATCH 05/12] docs: add oracle-learning example --- .../example/QAlgBase/testeg_QOracleLearning.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyqpanda-algorithm/example/QAlgBase/testeg_QOracleLearning.py diff --git a/pyqpanda-algorithm/example/QAlgBase/testeg_QOracleLearning.py b/pyqpanda-algorithm/example/QAlgBase/testeg_QOracleLearning.py new file mode 100644 index 00000000..c0dbbc45 --- /dev/null +++ b/pyqpanda-algorithm/example/QAlgBase/testeg_QOracleLearning.py @@ -0,0 +1,15 @@ +"""Deutsch-Jozsa and Bernstein-Vazirani example.""" + +from pyqpanda_alg import QOracleLearning + + +if __name__ == "__main__": + secret = "1011" # q0-first + recovered, probabilities = QOracleLearning.run_bernstein_vazirani(secret) + print("Bernstein-Vazirani secret:", recovered) + print("peak probability:", max(probabilities)) + + balanced_oracle = QOracleLearning.affine_truth_table("101") + classification, probabilities = QOracleLearning.run_deutsch_jozsa(balanced_oracle) + print("Deutsch-Jozsa:", classification) + print("P(0...0):", probabilities[0]) From 565a4bf93391eddbb46f2de045308c19399b35d8 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 14 Sep 2026 22:33:20 -0400 Subject: [PATCH 06/12] Integrate QWalk contribution onto fork develop --- docs/QWalk-discrete-time-cycle.md | 75 ++++ .../example/QAlgBase/testeg_QWalk.py | 22 + .../pyqpanda_alg/QWalk/__init__.py | 23 + .../pyqpanda_alg/QWalk/qwalk.py | 396 ++++++++++++++++++ pyqpanda-algorithm/pyqpanda_alg/__init__.py | 1 + test/QAlgBase/Test_QWalk.py | 120 ++++++ 6 files changed, 637 insertions(+) create mode 100644 docs/QWalk-discrete-time-cycle.md create mode 100644 pyqpanda-algorithm/example/QAlgBase/testeg_QWalk.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QWalk/__init__.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QWalk/qwalk.py create mode 100644 test/QAlgBase/Test_QWalk.py diff --git a/docs/QWalk-discrete-time-cycle.md b/docs/QWalk-discrete-time-cycle.md new file mode 100644 index 00000000..c7bf52b9 --- /dev/null +++ b/docs/QWalk-discrete-time-cycle.md @@ -0,0 +1,75 @@ +# QWalk: discrete-time coined quantum walk on a cycle + +`pyqpanda_alg.QWalk` adds a reusable circuit implementation of a discrete-time +coined quantum walk on a cyclic position register of size `N = 2^n`. + +## Step operator + +The state space is a coin qubit times an `n`-qubit position register. One step +is + +```text +U = S (H_coin ⊗ I_position) +``` + +by default. The conditional shift is + +```text +S |0, x> = |0, x-1 mod N> +S |1, x> = |1, x+1 mod N>. +``` + +The circuit does not allocate shift ancillas. Modular increment/decrement is +synthesized from multi-controlled `X` gates. `q_position[0]` is explicitly the +least-significant position bit, so an integer position has one unambiguous +mapping to the register. + +A custom coin gate/circuit can be supplied to `walk_step()` or +`coined_walk_cycle()`. The exact NumPy reference accepts a 2x2 unitary +`coin_matrix` for independent small-instance verification. + +## Quick start + +```python +from pyqpanda_alg.QWalk import CoinedQuantumWalkCycle + +walk = CoinedQuantumWalkCycle( + num_position_qubits=3, + steps=4, + initial_position=0, + initial_coin=0, +) + +expected = walk.reference() # NumPy-only independent evolution +observed = walk.run_exact() # exact PyQPanda3 StateVector evolution +``` + +For one Hadamard step from `|coin=0, position=0>` on an 8-cycle, the position +probability is exactly `1/2` at position 7 and `1/2` at position 1. After two +steps it is `1/4` at 6, `1/2` at 0, and `1/4` at 2, exposing the interference +that distinguishes the walk from a classical random walk. + +## Lower-level composition + +The module also exports: + +- `controlled_increment_cycle` / `controlled_decrement_cycle`: reversible + controlled modular arithmetic with no ancilla; +- `conditional_cycle_shift`: the coin-conditioned left/right shift; +- `walk_step`: one customizable coin-and-shift step; +- `coined_walk_cycle`: complete basis-state preparation plus repeated steps; +- `reference_walk_cycle`: independent exact NumPy evolution; +- `principal_displacement_moments`: explicit principal-branch mean/variance for + wrapped position distributions. + +## Complexity and scope + +For `n` position qubits, each shift uses `2n` controlled `X` operations plus two +coin flips for selecting the coin-0 decrement branch. A `t`-step walk therefore +uses `O(t n)` logical controlled operations. A multi-controlled `X` may itself +be decomposed by the backend, so hardware-native gate cost depends on the target +backend and transpilation strategy. + +This contribution implements the canonical one-dimensional coined walk on a +power-of-two cycle. It does not claim arbitrary graph compilation, decoherence +models, or asymptotic quantum speedup for a particular application. diff --git a/pyqpanda-algorithm/example/QAlgBase/testeg_QWalk.py b/pyqpanda-algorithm/example/QAlgBase/testeg_QWalk.py new file mode 100644 index 00000000..21f8f629 --- /dev/null +++ b/pyqpanda-algorithm/example/QAlgBase/testeg_QWalk.py @@ -0,0 +1,22 @@ +"""Small discrete-time coined quantum-walk example.""" + +from pyqpanda_alg.QWalk import CoinedQuantumWalkCycle + + +walk = CoinedQuantumWalkCycle( + num_position_qubits=3, + steps=4, + initial_position=0, + initial_coin=0, +) + +print("Independent exact reference distribution:") +for position, probability in enumerate(walk.reference()): + if probability > 1e-12: + print(f" position {position}: {probability:.6f}") + +# Requires PyQPanda3, like the other circuit examples in this repository. +print("PyQPanda3 exact circuit distribution:") +for position, probability in enumerate(walk.run_exact()): + if probability > 1e-12: + print(f" position {position}: {probability:.6f}") diff --git a/pyqpanda-algorithm/pyqpanda_alg/QWalk/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/QWalk/__init__.py new file mode 100644 index 00000000..5da56410 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QWalk/__init__.py @@ -0,0 +1,23 @@ +"""Discrete-time coined quantum walks on cyclic position registers.""" + +from .qwalk import ( + CoinedQuantumWalkCycle, + coined_walk_cycle, + conditional_cycle_shift, + controlled_decrement_cycle, + controlled_increment_cycle, + principal_displacement_moments, + reference_walk_cycle, + walk_step, +) + +__all__ = [ + "CoinedQuantumWalkCycle", + "coined_walk_cycle", + "conditional_cycle_shift", + "controlled_decrement_cycle", + "controlled_increment_cycle", + "principal_displacement_moments", + "reference_walk_cycle", + "walk_step", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QWalk/qwalk.py b/pyqpanda-algorithm/pyqpanda_alg/QWalk/qwalk.py new file mode 100644 index 00000000..cdd582c3 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QWalk/qwalk.py @@ -0,0 +1,396 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Discrete-time coined quantum walks on cyclic position registers. + +The quantum circuit path uses PyQPanda3 lazily so the independent NumPy +reference implementation remains usable in environments where PyQPanda3 is not +installed. Position qubits are interpreted little-endian: ``q_position[0]`` is +the least-significant bit of the integer position. +""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from numbers import Integral +from typing import Any + +import numpy as np + + +_HADAMARD = np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex) / np.sqrt(2.0) + + +def _require_nonnegative_int(name: str, value: int) -> int: + if isinstance(value, bool) or not isinstance(value, Integral): + raise TypeError(f"{name} must be an integer") + value = int(value) + if value < 0: + raise ValueError(f"{name} must be non-negative") + return value + + +def _validate_register(q_position: Sequence[Any], q_coin: Any) -> list[Any]: + q_position = list(q_position) + if not q_position: + raise ValueError("q_position must contain at least one qubit") + for index, qubit in enumerate(q_position): + if qubit in q_position[:index]: + raise ValueError("q_position must not contain duplicate qubits") + if q_coin in q_position: + raise ValueError("q_coin must be distinct from all position qubits") + return q_position + + +def _load_qpanda_gates(): + """Import the small PyQPanda3 surface needed by the circuit builders.""" + + try: + from pyqpanda3.core import QCircuit, H, X + except ImportError as exc: # pragma: no cover - dependency boundary + raise ImportError( + "PyQPanda3 is required for QWalk circuit construction; " + "reference_walk_cycle() remains available without it" + ) from exc + return QCircuit, H, X + + +def controlled_increment_cycle(q_position: Sequence[Any], q_control: Any): + """Return a controlled ``+1 mod 2**n`` circuit on ``q_position``. + + ``q_position[0]`` is the least-significant bit. The increment is active + only when ``q_control`` is ``|1>``. No ancilla qubits are required. + """ + + q_position = list(q_position) + if not q_position: + raise ValueError("q_position must contain at least one qubit") + if q_control in q_position: + raise ValueError("q_control must be distinct from all position qubits") + + QCircuit, _, X = _load_qpanda_gates() + circuit = QCircuit() + + # Toggle a high bit iff every lower bit was 1 before the increment. Walking + # from high to low preserves those lower control values until they are used. + for target in range(len(q_position) - 1, 0, -1): + controls = [q_control] + q_position[:target] + circuit << X(q_position[target]).control(controls) + circuit << X(q_position[0]).control(q_control) + return circuit + + +def controlled_decrement_cycle(q_position: Sequence[Any], q_control: Any): + """Return a controlled ``-1 mod 2**n`` circuit on ``q_position``. + + This is the exact inverse of :func:`controlled_increment_cycle`; all gates + are self-inverse, so reversing their order implements modular decrement. + """ + + q_position = list(q_position) + if not q_position: + raise ValueError("q_position must contain at least one qubit") + if q_control in q_position: + raise ValueError("q_control must be distinct from all position qubits") + + QCircuit, _, X = _load_qpanda_gates() + circuit = QCircuit() + circuit << X(q_position[0]).control(q_control) + for target in range(1, len(q_position)): + controls = [q_control] + q_position[:target] + circuit << X(q_position[target]).control(controls) + return circuit + + +def conditional_cycle_shift(q_position: Sequence[Any], q_coin: Any): + """Build the coined-walk shift on a cycle. + + Coin ``|0>`` moves the walker one site left and coin ``|1>`` moves it one + site right, with wraparound modulo ``2**len(q_position)``. + """ + + q_position = _validate_register(q_position, q_coin) + QCircuit, _, X = _load_qpanda_gates() + circuit = QCircuit() + + # Activate the decrement on the original coin-|0> subspace by temporarily + # flipping the coin. Restore it before acting on the coin-|1> subspace. + circuit << X(q_coin) + circuit << controlled_decrement_cycle(q_position, q_coin) + circuit << X(q_coin) + circuit << controlled_increment_cycle(q_position, q_coin) + return circuit + + +def walk_step( + q_position: Sequence[Any], + q_coin: Any, + coin_operator: Callable[[Any], Any] | None = None, +): + """Build one coined-walk step: coin operation followed by cyclic shift. + + Parameters + ---------- + q_position: + Position register in little-endian order. + q_coin: + Coin qubit, distinct from every position qubit. + coin_operator: + Optional callable receiving ``q_coin`` and returning a PyQPanda gate or + circuit. The default is a Hadamard coin. + """ + + q_position = _validate_register(q_position, q_coin) + QCircuit, H, _ = _load_qpanda_gates() + circuit = QCircuit() + circuit << (H(q_coin) if coin_operator is None else coin_operator(q_coin)) + circuit << conditional_cycle_shift(q_position, q_coin) + return circuit + + +def coined_walk_cycle( + q_position: Sequence[Any], + q_coin: Any, + steps: int, + initial_position: int = 0, + initial_coin: int = 0, + coin_operator: Callable[[Any], Any] | None = None, +): + """Build a complete discrete-time coined quantum walk on a cycle. + + The circuit prepares a computational-basis initial position and coin, then + applies ``steps`` repetitions of ``coin -> conditional shift``. + + Parameters + ---------- + q_position: + Position register. ``q_position[0]`` is the least-significant bit. + q_coin: + Coin qubit, distinct from every position qubit. + steps: + Number of walk steps. Zero returns state preparation only. + initial_position: + Integer in ``[0, 2**len(q_position))``. + initial_coin: + Either 0 or 1. + coin_operator: + Optional custom coin gate/circuit callable. Default: Hadamard. + """ + + q_position = _validate_register(q_position, q_coin) + steps = _require_nonnegative_int("steps", steps) + initial_position = _require_nonnegative_int("initial_position", initial_position) + if initial_position >= 2 ** len(q_position): + raise ValueError("initial_position does not fit in q_position") + if isinstance(initial_coin, bool) or not isinstance(initial_coin, Integral): + raise TypeError("initial_coin must be 0 or 1") + initial_coin = int(initial_coin) + if initial_coin not in (0, 1): + raise ValueError("initial_coin must be 0 or 1") + + QCircuit, _, X = _load_qpanda_gates() + circuit = QCircuit() + + for bit, qubit in enumerate(q_position): + if (initial_position >> bit) & 1: + circuit << X(qubit) + if initial_coin: + circuit << X(q_coin) + + for _ in range(steps): + circuit << walk_step(q_position, q_coin, coin_operator=coin_operator) + return circuit + + +def _validated_coin_matrix(coin_matrix: np.ndarray | None) -> np.ndarray: + if coin_matrix is None: + return _HADAMARD + matrix = np.asarray(coin_matrix, dtype=complex) + if matrix.shape != (2, 2): + raise ValueError("coin_matrix must have shape (2, 2)") + if not np.all(np.isfinite(matrix)): + raise ValueError("coin_matrix must contain finite values") + if not np.allclose(matrix.conj().T @ matrix, np.eye(2), atol=1e-12, rtol=1e-12): + raise ValueError("coin_matrix must be unitary") + return matrix + + +def reference_walk_cycle( + num_position_qubits: int, + steps: int, + initial_position: int = 0, + initial_coin: int = 0, + coin_matrix: np.ndarray | None = None, + return_state: bool = False, +): + """Exact NumPy reference for the same coined walk. + + This function intentionally has no PyQPanda dependency. It is both a small + problem reference oracle and a convenient way to reason about expected + distributions before constructing a quantum circuit. + + The returned state, when requested, has shape ``(2, 2**n)`` and is indexed + as ``state[coin, position]``. + """ + + num_position_qubits = _require_nonnegative_int( + "num_position_qubits", num_position_qubits + ) + if num_position_qubits == 0: + raise ValueError("num_position_qubits must be at least 1") + steps = _require_nonnegative_int("steps", steps) + initial_position = _require_nonnegative_int("initial_position", initial_position) + size = 2**num_position_qubits + if initial_position >= size: + raise ValueError("initial_position does not fit in the position register") + if isinstance(initial_coin, bool) or not isinstance(initial_coin, Integral): + raise TypeError("initial_coin must be 0 or 1") + initial_coin = int(initial_coin) + if initial_coin not in (0, 1): + raise ValueError("initial_coin must be 0 or 1") + + coin_matrix = _validated_coin_matrix(coin_matrix) + state = np.zeros((2, size), dtype=complex) + state[initial_coin, initial_position] = 1.0 + + for _ in range(steps): + state = coin_matrix @ state + shifted = np.zeros_like(state) + shifted[0] = np.roll(state[0], -1) # coin 0 -> left + shifted[1] = np.roll(state[1], 1) # coin 1 -> right + state = shifted + + if return_state: + return state + probabilities = np.sum(np.abs(state) ** 2, axis=0) + # Numerical cleanup keeps the public contract exact enough for downstream + # invariants without hiding a genuine normalization defect. + probabilities[np.abs(probabilities) < 1e-15] = 0.0 + return probabilities + + +def principal_displacement_moments( + probabilities: Sequence[float], origin: int = 0 +) -> tuple[float, float]: + """Return mean and variance of principal signed displacement on the cycle. + + For an even cycle, the antipodal site is assigned displacement ``-N/2``. + This branch convention is explicit because a circular walk does not have a + globally unique linear mean. + """ + + probabilities = np.asarray(probabilities, dtype=float) + if probabilities.ndim != 1 or probabilities.size == 0: + raise ValueError("probabilities must be a non-empty one-dimensional sequence") + if not np.all(np.isfinite(probabilities)) or np.any(probabilities < 0): + raise ValueError("probabilities must be finite and non-negative") + total = float(np.sum(probabilities)) + if total <= 0: + raise ValueError("probabilities must have positive total weight") + + size = int(probabilities.size) + if isinstance(origin, bool) or not isinstance(origin, Integral): + raise TypeError("origin must be an integer") + origin = int(origin) % size + p = probabilities / total + displacement = ((np.arange(size) - origin + size // 2) % size) - size // 2 + mean = float(np.dot(p, displacement)) + variance = float(np.dot(p, (displacement - mean) ** 2)) + return mean, variance + + +class CoinedQuantumWalkCycle: + """Convenience object for a Hadamard coined walk on a ``2**n`` cycle. + + ``circuit()`` builds the PyQPanda3 circuit. ``reference()`` returns the + exact independent NumPy distribution and therefore remains usable when + PyQPanda3 is unavailable. + """ + + def __init__( + self, + num_position_qubits: int, + steps: int, + initial_position: int = 0, + initial_coin: int = 0, + ): + num_position_qubits = _require_nonnegative_int( + "num_position_qubits", num_position_qubits + ) + if num_position_qubits == 0: + raise ValueError("num_position_qubits must be at least 1") + self.num_position_qubits = num_position_qubits + self.steps = _require_nonnegative_int("steps", steps) + self.initial_position = _require_nonnegative_int( + "initial_position", initial_position + ) + if self.initial_position >= 2**self.num_position_qubits: + raise ValueError("initial_position does not fit in the position register") + if isinstance(initial_coin, bool) or not isinstance(initial_coin, Integral): + raise TypeError("initial_coin must be 0 or 1") + self.initial_coin = int(initial_coin) + if self.initial_coin not in (0, 1): + raise ValueError("initial_coin must be 0 or 1") + + @property + def size(self) -> int: + return 2**self.num_position_qubits + + def circuit(self): + """Build the walk on contiguous qubits ``0..n`` (coin is qubit ``n``).""" + + q_position = list(range(self.num_position_qubits)) + q_coin = self.num_position_qubits + return coined_walk_cycle( + q_position, + q_coin, + self.steps, + initial_position=self.initial_position, + initial_coin=self.initial_coin, + ) + + def reference(self) -> np.ndarray: + """Return the exact independent position distribution.""" + + return reference_walk_cycle( + self.num_position_qubits, + self.steps, + initial_position=self.initial_position, + initial_coin=self.initial_coin, + ) + + def run_exact(self) -> np.ndarray: + """Simulate the PyQPanda circuit exactly and return position probability. + + The coin is the most-significant qubit in this convenience layout, so + the little-endian state vector reshapes to ``(coin, position)``. + """ + + try: + from pyqpanda3.quantum_info import StateVector + except ImportError as exc: # pragma: no cover - dependency boundary + raise ImportError("PyQPanda3 is required for run_exact()") from exc + + state = np.asarray( + StateVector(self.num_position_qubits + 1).evolve(self.circuit()).ndarray(), + dtype=complex, + ).reshape(2, self.size) + probabilities = np.sum(np.abs(state) ** 2, axis=0) + probabilities[np.abs(probabilities) < 1e-15] = 0.0 + return probabilities + + def moments(self) -> tuple[float, float]: + """Return reference mean/variance of principal displacement.""" + + return principal_displacement_moments( + self.reference(), origin=self.initial_position + ) diff --git a/pyqpanda-algorithm/pyqpanda_alg/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/__init__.py index 12d68083..d40572a1 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/__init__.py @@ -51,4 +51,5 @@ from . import Grover from . import QmRMR from . import QSEncode +from . import QWalk diff --git a/test/QAlgBase/Test_QWalk.py b/test/QAlgBase/Test_QWalk.py new file mode 100644 index 00000000..26f68337 --- /dev/null +++ b/test/QAlgBase/Test_QWalk.py @@ -0,0 +1,120 @@ +import importlib.util +from pathlib import Path +import sys + +import numpy as np +import pytest + + +MODULE = ( + Path(__file__).resolve().parents[2] + / "pyqpanda-algorithm" + / "pyqpanda_alg" + / "QWalk" + / "qwalk.py" +) +spec = importlib.util.spec_from_file_location("qwalk_under_test", MODULE) +qwalk = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = qwalk +spec.loader.exec_module(qwalk) + + +def test_reference_zero_steps_is_basis_state(): + p = qwalk.reference_walk_cycle(3, 0, initial_position=5, initial_coin=1) + expected = np.zeros(8) + expected[5] = 1.0 + np.testing.assert_allclose(p, expected, atol=0.0, rtol=0.0) + + +def test_reference_one_step_wraps_left_and_right(): + p = qwalk.reference_walk_cycle(3, 1, initial_position=0, initial_coin=0) + expected = np.zeros(8) + expected[7] = 0.5 + expected[1] = 0.5 + np.testing.assert_allclose(p, expected, atol=1e-15, rtol=0.0) + + +def test_reference_two_steps_has_interference_distribution(): + p = qwalk.reference_walk_cycle(3, 2, initial_position=0, initial_coin=0) + expected = np.zeros(8) + expected[6] = 0.25 + expected[0] = 0.50 + expected[2] = 0.25 + np.testing.assert_allclose(p, expected, atol=1e-15, rtol=0.0) + + +@pytest.mark.parametrize("qubits", [1, 2, 3, 4, 5]) +@pytest.mark.parametrize("steps", [0, 1, 2, 3, 7]) +def test_reference_is_normalized(qubits, steps): + p = qwalk.reference_walk_cycle( + qubits, + steps, + initial_position=(2**qubits - 1) // 2, + initial_coin=steps % 2, + ) + assert np.all(p >= 0) + assert np.sum(p) == pytest.approx(1.0, abs=1e-12) + + +def test_custom_unitary_coin_and_state_contract(): + x_coin = np.array([[0, 1], [1, 0]], dtype=complex) + state = qwalk.reference_walk_cycle( + 3, 3, initial_position=2, initial_coin=0, coin_matrix=x_coin, return_state=True + ) + assert state.shape == (2, 8) + assert np.sum(np.abs(state) ** 2) == pytest.approx(1.0) + # X alternates the direction: right, left, right from position 2 -> 3. + assert np.sum(np.abs(state[:, 3]) ** 2) == pytest.approx(1.0) + + +def test_nonunitary_coin_rejected(): + with pytest.raises(ValueError, match="unitary"): + qwalk.reference_walk_cycle(2, 1, coin_matrix=np.ones((2, 2))) + + +def test_principal_displacement_moments_are_explicit_at_wraparound(): + p = np.zeros(8) + p[7] = 0.5 # -1 from origin 0 + p[1] = 0.5 # +1 from origin 0 + mean, variance = qwalk.principal_displacement_moments(p, origin=0) + assert mean == pytest.approx(0.0) + assert variance == pytest.approx(1.0) + + +def test_convenience_object_matches_functional_reference(): + walk = qwalk.CoinedQuantumWalkCycle(4, 5, initial_position=3, initial_coin=1) + np.testing.assert_allclose( + walk.reference(), + qwalk.reference_walk_cycle(4, 5, initial_position=3, initial_coin=1), + ) + assert walk.size == 16 + mean, variance = walk.moments() + assert np.isfinite(mean) + assert variance >= 0 + + +@pytest.mark.parametrize( + "args, error", + [ + ((0, 1), ValueError), + ((2, -1), ValueError), + ((2, 1, 4), ValueError), + ((2, 1, 0, 2), ValueError), + ], +) +def test_reference_validation(args, error): + with pytest.raises(error): + qwalk.reference_walk_cycle(*args) + + +def test_native_statevector_matches_independent_reference_when_available(): + pytest.importorskip("pyqpanda3") + for qubits, steps, position, coin in [ + (1, 1, 0, 0), + (2, 3, 1, 1), + (3, 4, 7, 0), + ]: + walk = qwalk.CoinedQuantumWalkCycle(qubits, steps, position, coin) + np.testing.assert_allclose( + walk.run_exact(), walk.reference(), atol=1e-10, rtol=1e-10 + ) From 0c10560b6927bc9ed7255098ff4920b100d8c1a4 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 14 Sep 2026 22:47:16 -0400 Subject: [PATCH 07/12] Integrate QEC repetition-code contribution onto fork develop --- docs/QEC-three-qubit-repetition.md | 138 ++++++ .../example/QAlgBase/testeg_QEC_repetition.py | 23 + .../pyqpanda_alg/QEC/__init__.py | 29 ++ .../pyqpanda_alg/QEC/repetition_code.py | 413 ++++++++++++++++++ pyqpanda-algorithm/pyqpanda_alg/__init__.py | 1 + test/QAlgBase/Test_QEC_repetition.py | 231 ++++++++++ 6 files changed, 835 insertions(+) create mode 100644 docs/QEC-three-qubit-repetition.md create mode 100644 pyqpanda-algorithm/example/QAlgBase/testeg_QEC_repetition.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QEC/__init__.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QEC/repetition_code.py create mode 100644 test/QAlgBase/Test_QEC_repetition.py diff --git a/docs/QEC-three-qubit-repetition.md b/docs/QEC-three-qubit-repetition.md new file mode 100644 index 00000000..fbb81550 --- /dev/null +++ b/docs/QEC-three-qubit-repetition.md @@ -0,0 +1,138 @@ +# QEC: three-qubit repetition codes + +`pyqpanda_alg.QEC` adds the canonical three-qubit repetition code in two +complementary forms: + +- **bit-flip code**: corrects one Pauli-X error; +- **phase-flip code**: applies the same repetition logic in the Hadamard basis + and corrects one Pauli-Z error. + +These are intentionally small, inspectable quantum error-correction building +blocks. They demonstrate encoding, syndrome formation and coherent recovery +without claiming a general fault-tolerant stack. + +## Qubit contract + +The public circuit functions receive exactly three distinct qubits: + +```text +q_code = [q0, q1, q2] +``` + +Before encoding, `q0` contains the arbitrary logical state +`alpha|0> + beta|1>` and `q1=q2=|0>`. + +After recovery, the decoded logical state is again on `q0`. The two former +ancillas retain a deterministic syndrome in `(q1, q2)`: + +| error | syndrome `q1 q2` | syndrome integer `q1 + 2*q2` | +| --- | --- | ---: | +| none | `00` | 0 | +| on `q0` | `11` | 3 | +| on `q1` | `10` | 1 | +| on `q2` | `01` | 2 | + +No measurement or reset is assumed by the recovery circuit; the syndrome stays +coherently encoded in the ancillas. + +## Bit-flip code + +Encoding uses two CNOTs: + +```text +alpha|000> + beta|100> --encode--> alpha|000> + beta|111> +``` + +where the ket ordering in the equation is conceptual `(q0,q1,q2)` ordering. +The circuit API itself uses the supplied qubit objects directly and does not +rely on a state-vector bitstring display convention. + +Recovery applies: + +```text +CNOT(q0, q1) +CNOT(q0, q2) +TOFFOLI(q1, q2, q0) +``` + +For any one X error, this returns the original logical state to `q0` and leaves +the corresponding syndrome in `q1,q2`. + +```python +from pyqpanda_alg.QEC import bit_flip_encode, bit_flip_recover + +q = [0, 1, 2] +encode = bit_flip_encode(q) +recover = bit_flip_recover(q) +``` + +## Phase-flip code + +The phase-flip encoder first creates the ordinary repetition code and then +applies `H` to all three code qubits. A Z error in that basis is mapped to an X +error by another layer of Hadamards during recovery, after which the bit-flip +recovery circuit is reused. + +```python +from pyqpanda_alg.QEC import phase_flip_encode, phase_flip_recover + +q = [0, 1, 2] +encode = phase_flip_encode(q) +recover = phase_flip_recover(q) +``` + +## Independent NumPy reference + +`reference_repetition_recovery()` performs exact three-qubit state-vector +evolution without importing PyQPanda3. It is intended as an independent small +problem oracle for tests and examples. + +```python +import numpy as np +from pyqpanda_alg.QEC import reference_repetition_recovery + +logical = np.array([1, 1j], dtype=complex) / np.sqrt(2) +result = reference_repetition_recovery( + "phase_flip", + logical_state=logical, + error_qubit=2, +) + +print(result["logical_fidelity"]) # ~1.0 +print(result["syndrome_probabilities"]) # [0, 0, 1, 0] +``` + +The reference exposes: + +- the reduced density matrix of decoded `q0`; +- pure-state logical fidelity; +- syndrome probabilities; +- optional final three-qubit state vector; +- explicit `X`/`Z` override so callers can demonstrate the error-model + boundary rather than accidentally imply broader correction. + +## Capability boundary + +A three-qubit repetition code has distance three only for the selected error +basis: + +- the bit-flip code corrects one **X** error but does not generally correct Z; +- the phase-flip code corrects one **Z** error but does not generally correct X. + +This module does **not** claim to correct an arbitrary single-qubit Pauli error. +A construction such as the nine-qubit Shor code combines both mechanisms for a +broader error model; that construction is outside this module's scope. + +## Gate cost + +Ignoring error injection itself: + +| operation | CNOT | TOFFOLI | H | +| --- | ---: | ---: | ---: | +| bit-flip encode | 2 | 0 | 0 | +| bit-flip recover | 2 | 1 | 0 | +| phase-flip encode | 2 | 0 | 3 | +| phase-flip recover | 2 | 1 | 3 | + +Backend-native cost for the Toffoli depends on backend decomposition and +transpilation. diff --git a/pyqpanda-algorithm/example/QAlgBase/testeg_QEC_repetition.py b/pyqpanda-algorithm/example/QAlgBase/testeg_QEC_repetition.py new file mode 100644 index 00000000..80109910 --- /dev/null +++ b/pyqpanda-algorithm/example/QAlgBase/testeg_QEC_repetition.py @@ -0,0 +1,23 @@ +"""Three-qubit repetition-code reference example.""" + +import numpy as np + +from pyqpanda_alg.QEC import reference_repetition_recovery + + +logical = np.array([1.0, 1.0j], dtype=complex) / np.sqrt(2.0) + +for code in ("bit_flip", "phase_flip"): + print(f"{code}:") + for error_qubit in (None, 0, 1, 2): + result = reference_repetition_recovery( + code, + logical_state=logical, + error_qubit=error_qubit, + ) + syndrome = int(np.argmax(result["syndrome_probabilities"])) + print( + f" error={error_qubit!r} " + f"fidelity={result['logical_fidelity']:.12f} " + f"syndrome={syndrome}" + ) diff --git a/pyqpanda-algorithm/pyqpanda_alg/QEC/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/QEC/__init__.py new file mode 100644 index 00000000..05dc0d68 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QEC/__init__.py @@ -0,0 +1,29 @@ +"""Three-qubit repetition quantum error-correction codes.""" + +from .repetition_code import ( + ThreeQubitRepetitionCode, + bit_flip_encode, + bit_flip_recover, + expected_syndrome, + inject_single_pauli_error, + logical_density_matrix, + logical_fidelity, + phase_flip_encode, + phase_flip_recover, + reference_repetition_recovery, + syndrome_probabilities, +) + +__all__ = [ + "ThreeQubitRepetitionCode", + "bit_flip_encode", + "bit_flip_recover", + "expected_syndrome", + "inject_single_pauli_error", + "logical_density_matrix", + "logical_fidelity", + "phase_flip_encode", + "phase_flip_recover", + "reference_repetition_recovery", + "syndrome_probabilities", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QEC/repetition_code.py b/pyqpanda-algorithm/pyqpanda_alg/QEC/repetition_code.py new file mode 100644 index 00000000..17f55b68 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QEC/repetition_code.py @@ -0,0 +1,413 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Three-qubit repetition quantum error-correction codes. + +The module implements the canonical three-qubit bit-flip code and its +Hadamard-basis phase-flip counterpart. Circuit construction uses PyQPanda3 +lazily, while an independent NumPy state-vector reference remains usable +without PyQPanda3. + +Qubit convention +---------------- +``q_code[0]`` carries the logical input before encoding. ``q_code[1]`` and +``q_code[2]`` must begin in ``|0>``. Recovery decodes the logical state back to +``q_code[0]`` and leaves the two syndrome bits in ``q_code[1:3]``. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from numbers import Integral +from typing import Any + +import numpy as np + + +_H = np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex) / np.sqrt(2.0) +_X = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) +_Z = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) + + +def _validate_code_qubits(q_code: Sequence[Any]) -> list[Any]: + q_code = list(q_code) + if len(q_code) != 3: + raise ValueError("q_code must contain exactly three qubits") + for index, qubit in enumerate(q_code): + if qubit in q_code[:index]: + raise ValueError("q_code must not contain duplicate qubits") + return q_code + + +def _load_qpanda_gates(): + """Import the PyQPanda3 gates used by the circuit builders.""" + + try: + from pyqpanda3.core import QCircuit, CNOT, H, TOFFOLI, X, Z + except ImportError as exc: # pragma: no cover - dependency boundary + raise ImportError( + "PyQPanda3 is required for QEC circuit construction; the NumPy " + "reference helpers remain available without it" + ) from exc + return QCircuit, CNOT, H, TOFFOLI, X, Z + + +def bit_flip_encode(q_code: Sequence[Any]): + """Encode one logical qubit into the three-qubit repetition code. + + The input is ``alpha|0> + beta|1>`` on ``q_code[0]`` with the other two + qubits initialized to ``|0>``. The output is + ``alpha|000> + beta|111>``. + """ + + q0, q1, q2 = _validate_code_qubits(q_code) + QCircuit, CNOT, _, _, _, _ = _load_qpanda_gates() + circuit = QCircuit() + circuit << CNOT(q0, q1) + circuit << CNOT(q0, q2) + return circuit + + +def bit_flip_recover(q_code: Sequence[Any]): + """Decode and coherently recover from any one Pauli-X error. + + Recovery returns the logical state to ``q_code[0]`` and retains syndrome + information in ``(q_code[1], q_code[2])``. The syndrome convention is:: + + no error -> 00 + X on q0 -> 11 + X on q1 -> 10 + X on q2 -> 01 + + where the printed order is ``q1 q2``. + """ + + q0, q1, q2 = _validate_code_qubits(q_code) + QCircuit, CNOT, _, TOFFOLI, _, _ = _load_qpanda_gates() + circuit = QCircuit() + circuit << CNOT(q0, q1) + circuit << CNOT(q0, q2) + circuit << TOFFOLI(q1, q2, q0) + return circuit + + +def phase_flip_encode(q_code: Sequence[Any]): + """Encode the logical qubit into the Hadamard-basis repetition code. + + This applies :func:`bit_flip_encode` and then Hadamard gates to all three + code qubits. The code corrects one Pauli-Z error. + """ + + q_code = _validate_code_qubits(q_code) + QCircuit, _, H, _, _, _ = _load_qpanda_gates() + circuit = QCircuit() + circuit << bit_flip_encode(q_code) + for qubit in q_code: + circuit << H(qubit) + return circuit + + +def phase_flip_recover(q_code: Sequence[Any]): + """Decode and coherently recover from any one Pauli-Z error. + + Hadamards convert phase flips to bit flips, after which the ordinary + repetition decoder/recovery is applied. The decoded logical state is left + on ``q_code[0]`` with the same syndrome convention as bit-flip recovery. + """ + + q_code = _validate_code_qubits(q_code) + QCircuit, _, H, _, _, _ = _load_qpanda_gates() + circuit = QCircuit() + for qubit in q_code: + circuit << H(qubit) + circuit << bit_flip_recover(q_code) + return circuit + + +def inject_single_pauli_error( + q_code: Sequence[Any], error_qubit: int, pauli: str +): + """Return a circuit applying one explicit ``X`` or ``Z`` error. + + This helper is intended for examples, tests, and controlled experiments; + it is not a noise model. + """ + + q_code = _validate_code_qubits(q_code) + if isinstance(error_qubit, bool) or not isinstance(error_qubit, Integral): + raise TypeError("error_qubit must be an integer in [0, 2]") + error_qubit = int(error_qubit) + if error_qubit not in (0, 1, 2): + raise ValueError("error_qubit must be in [0, 2]") + if not isinstance(pauli, str): + raise TypeError("pauli must be 'X' or 'Z'") + pauli = pauli.upper() + if pauli not in ("X", "Z"): + raise ValueError("pauli must be 'X' or 'Z'") + + QCircuit, _, _, _, X, Z = _load_qpanda_gates() + circuit = QCircuit() + circuit << (X(q_code[error_qubit]) if pauli == "X" else Z(q_code[error_qubit])) + return circuit + + +def expected_syndrome(error_qubit: int | None) -> tuple[int, int]: + """Return the deterministic ``(q1, q2)`` syndrome for one correctable error.""" + + if error_qubit is None: + return (0, 0) + if isinstance(error_qubit, bool) or not isinstance(error_qubit, Integral): + raise TypeError("error_qubit must be None or an integer in [0, 2]") + error_qubit = int(error_qubit) + mapping = {0: (1, 1), 1: (1, 0), 2: (0, 1)} + if error_qubit not in mapping: + raise ValueError("error_qubit must be None or in [0, 2]") + return mapping[error_qubit] + + +def _validated_logical_state(logical_state: Sequence[complex]) -> np.ndarray: + state = np.asarray(logical_state, dtype=complex) + if state.shape != (2,): + raise ValueError("logical_state must contain exactly two amplitudes") + if not np.all(np.isfinite(state)): + raise ValueError("logical_state amplitudes must be finite") + norm = float(np.vdot(state, state).real) + if not np.isclose(norm, 1.0, atol=1e-12, rtol=1e-12): + raise ValueError("logical_state must be normalized") + return state.copy() + + +def _apply_single_qubit( + state: np.ndarray, gate: np.ndarray, qubit: int, num_qubits: int = 3 +) -> np.ndarray: + out = np.asarray(state, dtype=complex).copy() + step = 1 << qubit + block = step << 1 + for base in range(0, 1 << num_qubits, block): + for offset in range(step): + i0 = base + offset + i1 = i0 + step + a0, a1 = out[i0], out[i1] + out[i0] = gate[0, 0] * a0 + gate[0, 1] * a1 + out[i1] = gate[1, 0] * a0 + gate[1, 1] * a1 + return out + + +def _apply_controlled_x( + state: np.ndarray, + controls: Sequence[int], + target: int, + num_qubits: int = 3, +) -> np.ndarray: + out = np.asarray(state, dtype=complex).copy() + target_mask = 1 << target + control_mask = sum(1 << control for control in controls) + for basis in range(1 << num_qubits): + if basis & target_mask: + continue + if basis & control_mask == control_mask: + partner = basis | target_mask + out[basis], out[partner] = out[partner], out[basis] + return out + + +def _encode_reference(state: np.ndarray, code: str) -> np.ndarray: + state = _apply_controlled_x(state, [0], 1) + state = _apply_controlled_x(state, [0], 2) + if code == "phase_flip": + for qubit in range(3): + state = _apply_single_qubit(state, _H, qubit) + return state + + +def _recover_reference(state: np.ndarray, code: str) -> np.ndarray: + if code == "phase_flip": + for qubit in range(3): + state = _apply_single_qubit(state, _H, qubit) + state = _apply_controlled_x(state, [0], 1) + state = _apply_controlled_x(state, [0], 2) + state = _apply_controlled_x(state, [1, 2], 0) + return state + + +def logical_density_matrix(state: Sequence[complex]) -> np.ndarray: + """Trace out syndrome qubits and return the decoded q0 density matrix.""" + + state = np.asarray(state, dtype=complex) + if state.shape != (8,): + raise ValueError("state must be a three-qubit state vector of length 8") + norm = float(np.vdot(state, state).real) + if not np.isclose(norm, 1.0, atol=1e-10, rtol=1e-10): + raise ValueError("state vector must be normalized") + + density = np.zeros((2, 2), dtype=complex) + for syndrome in range(4): + base = syndrome << 1 # q0 is the least-significant qubit. + logical = state[[base, base | 1]] + density += np.outer(logical, logical.conj()) + return density + + +def syndrome_probabilities(state: Sequence[complex]) -> np.ndarray: + """Return probabilities for syndrome integer ``q1 + 2*q2``.""" + + state = np.asarray(state, dtype=complex) + if state.shape != (8,): + raise ValueError("state must be a three-qubit state vector of length 8") + norm = float(np.vdot(state, state).real) + if not np.isclose(norm, 1.0, atol=1e-10, rtol=1e-10): + raise ValueError("state vector must be normalized") + probabilities = np.empty(4, dtype=float) + for syndrome in range(4): + base = syndrome << 1 + probabilities[syndrome] = float( + np.abs(state[base]) ** 2 + np.abs(state[base | 1]) ** 2 + ) + probabilities[np.abs(probabilities) < 1e-15] = 0.0 + return probabilities + + +def logical_fidelity( + density_matrix: Sequence[Sequence[complex]], logical_state: Sequence[complex] +) -> float: + """Return fidelity against a pure target logical qubit state.""" + + density = np.asarray(density_matrix, dtype=complex) + if density.shape != (2, 2): + raise ValueError("density_matrix must have shape (2, 2)") + logical_state = _validated_logical_state(logical_state) + fidelity = float(np.vdot(logical_state, density @ logical_state).real) + if fidelity < 0 and fidelity > -1e-12: + fidelity = 0.0 + if fidelity > 1 and fidelity < 1 + 1e-12: + fidelity = 1.0 + return fidelity + + +def reference_repetition_recovery( + code: str, + logical_state: Sequence[complex] = (1.0, 0.0), + error_qubit: int | None = None, + error_pauli: str | None = None, + return_state: bool = False, +) -> dict[str, Any]: + """Exact NumPy reference for one encode/error/recover experiment. + + Parameters + ---------- + code: + ``"bit_flip"`` or ``"phase_flip"``. + logical_state: + Normalized amplitudes ``(alpha, beta)`` of the logical input qubit. + error_qubit: + ``None`` for no error or one of ``0, 1, 2``. + error_pauli: + Explicit ``"X"`` or ``"Z"``. When omitted, uses ``X`` for the bit-flip + code and ``Z`` for the phase-flip code. Supplying the opposite Pauli is + useful for demonstrating the code's documented error-model boundary. + return_state: + Include the final three-qubit state vector in the returned dictionary. + + Returns + ------- + dict + Contains ``logical_density``, ``logical_fidelity``, + ``syndrome_probabilities``, and metadata. For a correctable single error, + fidelity is one (within floating-point precision) for every logical + input state. + """ + + if code not in ("bit_flip", "phase_flip"): + raise ValueError("code must be 'bit_flip' or 'phase_flip'") + logical_state = _validated_logical_state(logical_state) + if error_qubit is not None: + if isinstance(error_qubit, bool) or not isinstance(error_qubit, Integral): + raise TypeError("error_qubit must be None or an integer in [0, 2]") + error_qubit = int(error_qubit) + if error_qubit not in (0, 1, 2): + raise ValueError("error_qubit must be None or in [0, 2]") + + correctable_pauli = "X" if code == "bit_flip" else "Z" + if error_pauli is None: + error_pauli = correctable_pauli + if not isinstance(error_pauli, str): + raise TypeError("error_pauli must be 'X' or 'Z'") + error_pauli = error_pauli.upper() + if error_pauli not in ("X", "Z"): + raise ValueError("error_pauli must be 'X' or 'Z'") + + state = np.zeros(8, dtype=complex) + state[0] = logical_state[0] + state[1] = logical_state[1] + state = _encode_reference(state, code) + + if error_qubit is not None: + gate = _X if error_pauli == "X" else _Z + state = _apply_single_qubit(state, gate, error_qubit) + + state = _recover_reference(state, code) + density = logical_density_matrix(state) + syndromes = syndrome_probabilities(state) + result: dict[str, Any] = { + "code": code, + "error_qubit": error_qubit, + "error_pauli": error_pauli if error_qubit is not None else None, + "correctable_pauli": correctable_pauli, + "logical_density": density, + "logical_fidelity": logical_fidelity(density, logical_state), + "syndrome_probabilities": syndromes, + } + if return_state: + result["state"] = state + return result + + +class ThreeQubitRepetitionCode: + """Convenience wrapper around the bit-flip or phase-flip repetition code.""" + + def __init__(self, code: str = "bit_flip"): + if code not in ("bit_flip", "phase_flip"): + raise ValueError("code must be 'bit_flip' or 'phase_flip'") + self.code = code + + @property + def correctable_pauli(self) -> str: + return "X" if self.code == "bit_flip" else "Z" + + def encode(self, q_code: Sequence[Any]): + return ( + bit_flip_encode(q_code) + if self.code == "bit_flip" + else phase_flip_encode(q_code) + ) + + def recover(self, q_code: Sequence[Any]): + return ( + bit_flip_recover(q_code) + if self.code == "bit_flip" + else phase_flip_recover(q_code) + ) + + def reference( + self, + logical_state: Sequence[complex] = (1.0, 0.0), + error_qubit: int | None = None, + error_pauli: str | None = None, + return_state: bool = False, + ) -> dict[str, Any]: + return reference_repetition_recovery( + self.code, + logical_state=logical_state, + error_qubit=error_qubit, + error_pauli=error_pauli, + return_state=return_state, + ) diff --git a/pyqpanda-algorithm/pyqpanda_alg/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/__init__.py index d40572a1..b805f027 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/__init__.py @@ -52,4 +52,5 @@ from . import QmRMR from . import QSEncode from . import QWalk +from . import QEC diff --git a/test/QAlgBase/Test_QEC_repetition.py b/test/QAlgBase/Test_QEC_repetition.py new file mode 100644 index 00000000..51d506a9 --- /dev/null +++ b/test/QAlgBase/Test_QEC_repetition.py @@ -0,0 +1,231 @@ +import importlib.util +from pathlib import Path +import sys + +import numpy as np +import pytest + + +MODULE = ( + Path(__file__).resolve().parents[2] + / "pyqpanda-algorithm" + / "pyqpanda_alg" + / "QEC" + / "repetition_code.py" +) +spec = importlib.util.spec_from_file_location("qec_under_test", MODULE) +qec = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = qec +spec.loader.exec_module(qec) + + +SQRT2 = np.sqrt(2.0) +LOGICAL_STATES = [ + np.array([1.0, 0.0], dtype=complex), + np.array([0.0, 1.0], dtype=complex), + np.array([1.0 / SQRT2, 1.0 / SQRT2], dtype=complex), + np.array([1.0 / SQRT2, -1.0 / SQRT2], dtype=complex), + np.array([1.0 / SQRT2, 1.0j / SQRT2], dtype=complex), +] + + +def syndrome_integer(error_qubit): + q1, q2 = qec.expected_syndrome(error_qubit) + return q1 + 2 * q2 + + +@pytest.mark.parametrize("code", ["bit_flip", "phase_flip"]) +@pytest.mark.parametrize("error_qubit", [None, 0, 1, 2]) +@pytest.mark.parametrize("logical_state", LOGICAL_STATES) +def test_all_single_correctable_errors_restore_arbitrary_logical_state( + code, error_qubit, logical_state +): + result = qec.reference_repetition_recovery( + code, + logical_state=logical_state, + error_qubit=error_qubit, + return_state=True, + ) + expected_density = np.outer(logical_state, logical_state.conj()) + np.testing.assert_allclose( + result["logical_density"], expected_density, atol=1e-12, rtol=1e-12 + ) + assert result["logical_fidelity"] == pytest.approx(1.0, abs=1e-12) + assert np.vdot(result["state"], result["state"]).real == pytest.approx(1.0) + + syndrome = np.zeros(4) + syndrome[syndrome_integer(error_qubit)] = 1.0 + np.testing.assert_allclose( + result["syndrome_probabilities"], syndrome, atol=1e-12, rtol=0.0 + ) + + +def test_expected_syndrome_table(): + assert qec.expected_syndrome(None) == (0, 0) + assert qec.expected_syndrome(0) == (1, 1) + assert qec.expected_syndrome(1) == (1, 0) + assert qec.expected_syndrome(2) == (0, 1) + + +def test_scope_boundary_bit_flip_code_does_not_correct_phase_error(): + logical = np.array([1.0 / SQRT2, 1.0 / SQRT2], dtype=complex) + result = qec.reference_repetition_recovery( + "bit_flip", logical, error_qubit=0, error_pauli="Z" + ) + assert result["logical_fidelity"] == pytest.approx(0.0, abs=1e-12) + + +def test_scope_boundary_phase_flip_code_does_not_correct_bit_error(): + logical = np.array([1.0 / SQRT2, 1.0j / SQRT2], dtype=complex) + result = qec.reference_repetition_recovery( + "phase_flip", logical, error_qubit=0, error_pauli="X" + ) + assert result["logical_fidelity"] < 1.0 - 1e-12 + + +def test_convenience_wrapper_matches_functional_reference(): + logical = np.array([1.0 / SQRT2, -1.0j / SQRT2], dtype=complex) + code = qec.ThreeQubitRepetitionCode("phase_flip") + assert code.correctable_pauli == "Z" + wrapped = code.reference(logical, error_qubit=2) + direct = qec.reference_repetition_recovery( + "phase_flip", logical, error_qubit=2 + ) + np.testing.assert_allclose(wrapped["logical_density"], direct["logical_density"]) + np.testing.assert_allclose( + wrapped["syndrome_probabilities"], direct["syndrome_probabilities"] + ) + + +@pytest.mark.parametrize( + "call, error", + [ + (lambda: qec.reference_repetition_recovery("bad"), ValueError), + (lambda: qec.reference_repetition_recovery("bit_flip", [1, 1]), ValueError), + (lambda: qec.reference_repetition_recovery("bit_flip", [1, 0], 3), ValueError), + ( + lambda: qec.reference_repetition_recovery( + "bit_flip", [1, 0], 0, error_pauli="Y" + ), + ValueError, + ), + (lambda: qec.expected_syndrome(4), ValueError), + ], +) +def test_reference_validation(call, error): + with pytest.raises(error): + call() + + +class FakeGate: + def __init__(self, name, *qubits): + self.name = name + self.qubits = tuple(qubits) + + +class FakeCircuit: + def __init__(self): + self.ops = [] + + def __lshift__(self, operation): + if isinstance(operation, FakeCircuit): + self.ops.extend(operation.ops) + else: + self.ops.append(operation) + return self + + +def fake_loader(): + return ( + FakeCircuit, + lambda c, t: FakeGate("CNOT", c, t), + lambda q: FakeGate("H", q), + lambda a, b, t: FakeGate("TOFFOLI", a, b, t), + lambda q: FakeGate("X", q), + lambda q: FakeGate("Z", q), + ) + + +def op_tuples(circuit): + return [(op.name, *op.qubits) for op in circuit.ops] + + +def test_circuit_plan_matches_repetition_code_algebra(monkeypatch): + monkeypatch.setattr(qec, "_load_qpanda_gates", fake_loader) + assert op_tuples(qec.bit_flip_encode([0, 1, 2])) == [ + ("CNOT", 0, 1), + ("CNOT", 0, 2), + ] + assert op_tuples(qec.bit_flip_recover([0, 1, 2])) == [ + ("CNOT", 0, 1), + ("CNOT", 0, 2), + ("TOFFOLI", 1, 2, 0), + ] + assert op_tuples(qec.phase_flip_encode([0, 1, 2])) == [ + ("CNOT", 0, 1), + ("CNOT", 0, 2), + ("H", 0), + ("H", 1), + ("H", 2), + ] + assert op_tuples(qec.phase_flip_recover([0, 1, 2])) == [ + ("H", 0), + ("H", 1), + ("H", 2), + ("CNOT", 0, 1), + ("CNOT", 0, 2), + ("TOFFOLI", 1, 2, 0), + ] + assert op_tuples(qec.inject_single_pauli_error([0, 1, 2], 2, "z")) == [ + ("Z", 2) + ] + + +def test_circuit_input_validation_without_pyqpanda(monkeypatch): + monkeypatch.setattr(qec, "_load_qpanda_gates", fake_loader) + with pytest.raises(ValueError, match="exactly three"): + qec.bit_flip_encode([0, 1]) + with pytest.raises(ValueError, match="duplicate"): + qec.bit_flip_encode([0, 0, 2]) + with pytest.raises(ValueError, match=r"\[0, 2\]"): + qec.inject_single_pauli_error([0, 1, 2], 3, "X") + + +def reduced_density_q0_native(state): + vector = np.asarray(state, dtype=complex).reshape(8) + density = np.zeros((2, 2), dtype=complex) + for syndrome in range(4): + base = syndrome << 1 + logical = vector[[base, base | 1]] + density += np.outer(logical, logical.conj()) + return density + + +def test_native_statevector_crosscheck_when_pyqpanda3_is_available(): + pytest.importorskip("pyqpanda3") + from pyqpanda3.core import QCircuit, H, X, Z + from pyqpanda3.quantum_info import StateVector + + target = np.array([1.0 / SQRT2, 1.0 / SQRT2], dtype=complex) + target_density = np.outer(target, target.conj()) + for code in ("bit_flip", "phase_flip"): + for error_qubit in (None, 0, 1, 2): + circuit = QCircuit() + circuit << H(0) + if code == "bit_flip": + circuit << qec.bit_flip_encode([0, 1, 2]) + if error_qubit is not None: + circuit << X(error_qubit) + circuit << qec.bit_flip_recover([0, 1, 2]) + else: + circuit << qec.phase_flip_encode([0, 1, 2]) + if error_qubit is not None: + circuit << Z(error_qubit) + circuit << qec.phase_flip_recover([0, 1, 2]) + state = StateVector(3).evolve(circuit).ndarray() + np.testing.assert_allclose( + reduced_density_q0_native(state), + target_density, + atol=1e-10, + rtol=1e-10, + ) From 09d50c0ae3c1df306bd9f3c2e310a95f9ba4713d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 20 Sep 2026 18:51:45 -0400 Subject: [PATCH 08/12] feat: add variational quantum linear solver --- pyqpanda-algorithm/pyqpanda_alg/VQLS/VQLS.py | 375 +++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQLS/VQLS.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQLS/VQLS.py b/pyqpanda-algorithm/pyqpanda_alg/VQLS/VQLS.py new file mode 100644 index 00000000..e7e94918 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQLS/VQLS.py @@ -0,0 +1,375 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Variational Quantum Linear Solver. + +The solver owns the backend-independent VQLS mathematics. A caller may provide +any ansatz function mapping a real parameter vector to a statevector, including +a PyQPanda3 simulator or hardware adapter. A compact RY plus CNOT-ring reference +ansatz is included for examples and local experimentation. +""" + +from dataclasses import dataclass +from typing import Callable, Optional, Sequence, Tuple + +import numpy as np + + +StateFunction = Callable[[np.ndarray], np.ndarray] + + +@dataclass(frozen=True) +class VQLSResult: + """Result returned by VQLS.solve.""" + + parameters: np.ndarray + state: np.ndarray + solution: np.ndarray + cost: float + residual_norm: float + iterations: int + converged: bool + cost_history: Tuple[float, ...] + + +def _normalize_state(state: Sequence[complex], dimension: int) -> np.ndarray: + vector = np.asarray(state, dtype=np.complex128).reshape(-1) + if vector.size != dimension: + raise ValueError( + "ansatz state has dimension {0}, expected {1}".format( + vector.size, dimension + ) + ) + norm = float(np.linalg.norm(vector)) + if not np.isfinite(norm) or norm <= np.finfo(float).tiny: + raise ValueError("ansatz returned a zero or non-finite state") + return vector / norm + + +def _apply_ry( + state: np.ndarray, angle: float, qubit: int, num_qubits: int +) -> None: + bit = 1 << qubit + cosine = float(np.cos(angle / 2.0)) + sine = float(np.sin(angle / 2.0)) + for index in range(1 << num_qubits): + if index & bit: + continue + paired = index | bit + low = state[index] + high = state[paired] + state[index] = cosine * low - sine * high + state[paired] = sine * low + cosine * high + + +def _apply_cnot( + state: np.ndarray, control: int, target: int, num_qubits: int +) -> None: + control_bit = 1 << control + target_bit = 1 << target + for index in range(1 << num_qubits): + if not (index & control_bit) or (index & target_bit): + continue + paired = index | target_bit + state[index], state[paired] = state[paired], state[index] + + +def ry_ring_state( + parameters: Sequence[float], num_qubits: int, layers: int = 2 +) -> np.ndarray: + """Return a hardware-efficient real-amplitude RY/CNOT-ring statevector. + + Qubit zero is the least-significant statevector bit. Every layer applies + one RY rotation per qubit followed by nearest-neighbour CNOTs. Systems with + three or more qubits close the entangling chain into a ring. + """ + + if num_qubits < 1: + raise ValueError("num_qubits must be at least 1") + if layers < 1: + raise ValueError("layers must be at least 1") + + params = np.asarray(parameters, dtype=float).reshape(-1) + expected = num_qubits * layers + if params.size != expected: + raise ValueError( + "RY-ring ansatz needs {0} parameters, received {1}".format( + expected, params.size + ) + ) + + state = np.zeros(1 << num_qubits, dtype=np.complex128) + state[0] = 1.0 + offset = 0 + for _ in range(layers): + for qubit in range(num_qubits): + _apply_ry(state, params[offset], qubit, num_qubits) + offset += 1 + for qubit in range(num_qubits - 1): + _apply_cnot(state, qubit, qubit + 1, num_qubits) + if num_qubits > 2: + _apply_cnot(state, num_qubits - 1, 0, num_qubits) + return state + + +class VQLS: + """Variational Quantum Linear Solver. + + The global objective is + + C(theta) = 1 - ||^2 / . + + It reaches zero when the variational state is parallel to A^-1 b. Because a + normalized quantum state cannot encode the classical solution magnitude, + solution() restores the least-squares-optimal complex scale before + reporting the residual. + + The optimizer is SPSA, so every update needs two gradient objective + evaluations regardless of parameter count. This keeps the optimizer useful + for shot-based backend adapters. + + Parameters + ---------- + matrix: + Square linear-system matrix. Its dimension must be a power of two. + Hermiticity is not required because the cost uses A^H A. + vector: + Non-zero right-hand-side vector. + state_fn: + Callable mapping a real parameter vector to a statevector. When omitted, + ry_ring_state is used. + num_parameters: + Parameter count for a custom state_fn. Inferred for the built-in ansatz. + layers: + Layers in the built-in RY/CNOT-ring ansatz. + max_iterations: + Maximum SPSA updates. + tolerance: + Cost considered converged. + learning_rate: + Initial SPSA learning-rate coefficient. + perturbation: + Initial SPSA finite-difference perturbation. + seed: + Random seed for reproducible perturbations and initialization. + parameter_bounds: + Optional clipping bounds. + gradient_clip: + Optional Euclidean gradient norm cap. + """ + + def __init__( + self, + matrix: Sequence[Sequence[complex]], + vector: Sequence[complex], + state_fn: Optional[StateFunction] = None, + num_parameters: Optional[int] = None, + layers: int = 2, + max_iterations: int = 250, + tolerance: float = 1e-6, + learning_rate: float = 0.2, + perturbation: float = 0.12, + seed: Optional[int] = None, + parameter_bounds: Optional[Tuple[float, float]] = (-np.pi, np.pi), + gradient_clip: Optional[float] = 10.0, + ): + matrix_array = np.asarray(matrix, dtype=np.complex128) + if matrix_array.ndim != 2 or matrix_array.shape[0] != matrix_array.shape[1]: + raise ValueError("matrix must be square") + dimension = int(matrix_array.shape[0]) + if dimension < 2 or dimension & (dimension - 1): + raise ValueError("matrix dimension must be a power of two >= 2") + if not np.all(np.isfinite(matrix_array)): + raise ValueError("matrix contains non-finite values") + + vector_array = np.asarray(vector, dtype=np.complex128).reshape(-1) + if vector_array.size != dimension: + raise ValueError("vector length must match matrix dimension") + if not np.all(np.isfinite(vector_array)): + raise ValueError("vector contains non-finite values") + vector_norm = float(np.linalg.norm(vector_array)) + if vector_norm <= np.finfo(float).tiny: + raise ValueError("vector must be non-zero") + + matrix_norm = float(np.linalg.norm(matrix_array, ord=2)) + if not np.isfinite(matrix_norm) or matrix_norm <= np.finfo(float).tiny: + raise ValueError("matrix must have non-zero finite spectral norm") + if np.linalg.matrix_rank(matrix_array) < dimension: + raise ValueError("matrix must be non-singular") + + num_qubits = int(np.log2(dimension)) + if state_fn is None: + if layers < 1: + raise ValueError("layers must be at least 1") + inferred_parameters = num_qubits * int(layers) + if num_parameters is not None and int(num_parameters) != inferred_parameters: + raise ValueError( + "built-in ansatz requires {0} parameters".format( + inferred_parameters + ) + ) + + def built_in(parameters: np.ndarray) -> np.ndarray: + return ry_ring_state(parameters, num_qubits, int(layers)) + + self._state_fn = built_in + self.num_parameters = inferred_parameters + else: + if num_parameters is None or int(num_parameters) < 1: + raise ValueError( + "num_parameters must be supplied for a custom state_fn" + ) + self._state_fn = state_fn + self.num_parameters = int(num_parameters) + + if int(max_iterations) < 1: + raise ValueError("max_iterations must be at least 1") + if not (0.0 < float(tolerance) < 1.0): + raise ValueError("tolerance must be between 0 and 1") + if float(learning_rate) <= 0.0 or float(perturbation) <= 0.0: + raise ValueError("learning_rate and perturbation must be positive") + if parameter_bounds is not None: + low, high = map(float, parameter_bounds) + if not np.isfinite(low) or not np.isfinite(high) or low >= high: + raise ValueError("parameter_bounds must satisfy finite low < high") + self.parameter_bounds = (low, high) + else: + self.parameter_bounds = None + if gradient_clip is not None and float(gradient_clip) <= 0.0: + raise ValueError("gradient_clip must be positive when supplied") + + self.matrix = matrix_array + self.vector = vector_array + self.dimension = dimension + self.num_qubits = num_qubits + self.max_iterations = int(max_iterations) + self.tolerance = float(tolerance) + self.learning_rate = float(learning_rate) + self.perturbation = float(perturbation) + self.seed = seed + self.gradient_clip = ( + None if gradient_clip is None else float(gradient_clip) + ) + self._normalized_vector = vector_array / vector_norm + self._scaled_matrix = matrix_array / matrix_norm + + def state(self, parameters: Sequence[float]) -> np.ndarray: + """Evaluate and normalize the configured variational ansatz.""" + + params = np.asarray(parameters, dtype=float).reshape(-1) + if params.size != self.num_parameters: + raise ValueError( + "expected {0} parameters, received {1}".format( + self.num_parameters, params.size + ) + ) + if not np.all(np.isfinite(params)): + raise ValueError("parameters contain non-finite values") + return _normalize_state(self._state_fn(params.copy()), self.dimension) + + def cost(self, parameters: Sequence[float]) -> float: + """Evaluate the scale-invariant global VQLS objective.""" + + state = self.state(parameters) + transformed = self._scaled_matrix @ state + denominator = float(np.vdot(transformed, transformed).real) + if not np.isfinite(denominator) or denominator <= np.finfo(float).tiny: + return 1.0 + overlap = np.vdot(self._normalized_vector, transformed) + value = 1.0 - (float(abs(overlap) ** 2) / denominator) + return float(np.clip(value, 0.0, 1.0)) + + def solution( + self, parameters: Sequence[float] + ) -> Tuple[np.ndarray, np.ndarray, float]: + """Return state, scaled solution, and original-system residual norm.""" + + state = self.state(parameters) + transformed = self.matrix @ state + denominator = np.vdot(transformed, transformed) + if abs(denominator) <= np.finfo(float).tiny: + raise ValueError("ansatz maps into a numerically null matrix direction") + scale = np.vdot(transformed, self.vector) / denominator + solution = scale * state + residual = float(np.linalg.norm(self.matrix @ solution - self.vector)) + return state, solution, residual + + def _project(self, parameters: np.ndarray) -> np.ndarray: + if self.parameter_bounds is None: + return parameters + low, high = self.parameter_bounds + return np.clip(parameters, low, high) + + def solve( + self, initial_parameters: Optional[Sequence[float]] = None + ) -> VQLSResult: + """Optimize the VQLS objective and return the best solution found.""" + + rng = np.random.default_rng(self.seed) + if initial_parameters is None: + parameters = rng.uniform( + -np.pi / 4.0, np.pi / 4.0, self.num_parameters + ) + else: + parameters = np.asarray(initial_parameters, dtype=float).reshape(-1) + if parameters.size != self.num_parameters: + raise ValueError( + "initial_parameters must contain {0} values".format( + self.num_parameters + ) + ) + if not np.all(np.isfinite(parameters)): + raise ValueError("initial_parameters contain non-finite values") + parameters = self._project(parameters.astype(float, copy=True)) + + best_parameters = parameters.copy() + best_cost = self.cost(parameters) + history = [] + iterations = 0 + + for step in range(1, self.max_iterations + 1): + if best_cost <= self.tolerance: + break + + ak = self.learning_rate / ((step + 10.0) ** 0.602) + ck = self.perturbation / (step ** 0.101) + delta = rng.choice(np.array([-1.0, 1.0]), self.num_parameters) + + plus = self._project(parameters + ck * delta) + minus = self._project(parameters - ck * delta) + gradient = ((self.cost(plus) - self.cost(minus)) / (2.0 * ck)) * delta + + if self.gradient_clip is not None: + gradient_norm = float(np.linalg.norm(gradient)) + if gradient_norm > self.gradient_clip: + gradient *= self.gradient_clip / gradient_norm + + parameters = self._project(parameters - ak * gradient) + current_cost = self.cost(parameters) + history.append(current_cost) + iterations = step + + if current_cost < best_cost: + best_cost = current_cost + best_parameters = parameters.copy() + + state, solution, residual = self.solution(best_parameters) + return VQLSResult( + parameters=best_parameters, + state=state, + solution=solution, + cost=float(best_cost), + residual_norm=residual, + iterations=iterations, + converged=bool(best_cost <= self.tolerance), + cost_history=tuple(float(value) for value in history), + ) From 822366f128f707f8daedb25080df03570375cfe0 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 20 Sep 2026 18:51:59 -0400 Subject: [PATCH 09/12] feat: export VQLS module --- pyqpanda-algorithm/pyqpanda_alg/VQLS/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pyqpanda-algorithm/pyqpanda_alg/VQLS/__init__.py diff --git a/pyqpanda-algorithm/pyqpanda_alg/VQLS/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/VQLS/__init__.py new file mode 100644 index 00000000..1efcadf1 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/VQLS/__init__.py @@ -0,0 +1,3 @@ +from .VQLS import VQLS, VQLSResult, ry_ring_state + +__all__ = ["VQLS", "VQLSResult", "ry_ring_state"] From 63880608b308714ef1d68b11e402bd3cd0fd7dd4 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 20 Sep 2026 18:52:01 -0400 Subject: [PATCH 10/12] docs: add VQLS linear-system example --- pyqpanda-algorithm/example/VQLS/demo_vqls.py | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 pyqpanda-algorithm/example/VQLS/demo_vqls.py diff --git a/pyqpanda-algorithm/example/VQLS/demo_vqls.py b/pyqpanda-algorithm/example/VQLS/demo_vqls.py new file mode 100644 index 00000000..e4b8a356 --- /dev/null +++ b/pyqpanda-algorithm/example/VQLS/demo_vqls.py @@ -0,0 +1,35 @@ +"""Minimal Variational Quantum Linear Solver example.""" + +import numpy as np + +from pyqpanda_alg.VQLS import VQLS + + +def main(): + matrix = np.array( + [ + [1.0, -1.0 / 3.0], + [-1.0 / 3.0, 1.0], + ] + ) + vector = np.array([1.0, 0.0]) + + solver = VQLS( + matrix, + vector, + layers=2, + max_iterations=400, + tolerance=1e-7, + learning_rate=0.35, + perturbation=0.15, + seed=7, + ) + result = solver.solve() + + print("cost:", result.cost) + print("residual norm:", result.residual_norm) + print("solution:", np.real_if_close(result.solution)) + + +if __name__ == "__main__": + main() From 93e26ac57a2540ee850d888f9e129ac8b4ed3f71 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 20 Sep 2026 18:52:12 -0400 Subject: [PATCH 11/12] feat: expose VQLS package --- pyqpanda-algorithm/pyqpanda_alg/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqpanda-algorithm/pyqpanda_alg/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/__init__.py index b805f027..9f49cc82 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/__init__.py @@ -51,6 +51,7 @@ from . import Grover from . import QmRMR from . import QSEncode +from . import VQLS from . import QWalk from . import QEC From 9fc787b06b8deb4b9886c4755340e24d9b1fd64f Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 20 Sep 2026 18:52:15 -0400 Subject: [PATCH 12/12] docs: document VQLS scientific computing support --- README_EN.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README_EN.md b/README_EN.md index d74c953c..be07c398 100644 --- a/README_EN.md +++ b/README_EN.md @@ -50,6 +50,8 @@ Integrates quantum computing into classical machine learning to improve efficien Solves key problems in physical modeling and engineering simulation (e.g., eigenvalues, linear equations, matrix decomposition). - **QSVD (Quantum Variational Singular Value Decomposition)** Extracts matrix singular values/vectors under a variational framework (for dimensionality reduction and recommendation systems). +- **VQLS (Variational Quantum Linear Solver)** + Solves power-of-two linear systems with a variational state ansatz, a scale-invariant global VQLS cost, SPSA optimization, and residual-aware reconstruction of the classical solution magnitude. Custom state functions can bridge PyQPanda3 simulators or hardware backends. ### 4. General Tools & Basic Components Provides underlying tools for quantum computing workflows.