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
78 changes: 78 additions & 0 deletions benchmarks/benchmark_qsvr_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Compare the original full kernel loop with symmetry reuse on CPUQVM.

Run from the repository root with PYTHONPATH=pyqpanda-algorithm.
This measures local simulator runtime, not quantum hardware speedup.
"""

import argparse
import importlib.metadata
import json
import platform
import statistics
from time import perf_counter

import numpy as np

from pyqpanda_alg.QSVR import Quantum_SVR


def dense_kernel(model, x):
matrix = np.empty((len(x), len(x)))
for i in range(len(x)):
for j in range(len(x)):
matrix[i, j] = model.dist(x[i], x[j])
return matrix


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sizes", nargs="+", type=int, default=[16, 32, 64])
parser.add_argument("--repeats", type=int, default=3)
args = parser.parse_args()
if args.repeats < 1 or any(size < 2 for size in args.sizes):
parser.error("repeats must be positive and sample sizes must be at least 2")

results = []
for size in args.sizes:
rng = np.random.default_rng(20260907 + size)
model = Quantum_SVR(rng.normal(size=(size, 2)), rng.normal(size=size))
x = model.x
model.dist(x[0], x[1]) # Warm the simulator before either timed path.
durations = {"dense": [], "symmetric": []}
max_error = 0.0
for repeat in range(args.repeats):
order = ["dense", "symmetric"]
if repeat % 2:
order.reverse()
matrices = {}
for method in order:
start = perf_counter()
matrices[method] = (dense_kernel(model, x) if method == "dense"
else model.k_kernel(x, x.copy()))
durations[method].append(perf_counter() - start)
np.testing.assert_allclose(matrices["dense"], matrices["symmetric"],
atol=1e-12, rtol=1e-12)
max_error = max(max_error, float(np.max(np.abs(
matrices["dense"] - matrices["symmetric"]))))
dense = statistics.median(durations["dense"])
symmetric = statistics.median(durations["symmetric"])
results.append({
"samples": size,
"dense_calls": size * size,
"symmetric_calls": size * (size + 1) // 2,
"dense_median_seconds": dense,
"symmetric_median_seconds": symmetric,
"speedup": dense / symmetric,
"max_absolute_error": max_error,
})
print(json.dumps({
"python": platform.python_version(),
"pyqpanda3": importlib.metadata.version("pyqpanda3"),
"numpy": np.__version__,
"repeats": args.repeats,
"results": results,
}, indent=2))


if __name__ == "__main__":
main()
73 changes: 73 additions & 0 deletions docs/qsvr-kernel-symmetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# QSVR training-kernel symmetry

## Release note

`Quantum_SVR.k_kernel(X, Y)` now evaluates only one triangle when `X` and
`Y` contain the same samples in the same order. The fidelity kernel satisfies
`K(x, y) = |<phi(y)|phi(x)>|^2 = K(y, x)`, so the other triangle can reuse
those values. Equal array copies and equal nested lists also benefit.

For `n` training samples, simulator calls decrease from `n^2` to
`n(n+1)/2`. Diagonal entries are still evaluated through `dist`; they are
not replaced by constants. Distinct datasets use the original full pairwise
evaluation, including square matrices with different sample order. There is
no persistent cache, and the public method signatures are unchanged.

The optimization applies to the existing symmetric fidelity kernel.
Custom `dist` overrides must preserve that symmetry. Differences from the
old full loop can occur at floating-point roundoff because one mirrored
entry replaces a separately simulated evaluation.

## Validation

Base: `OriginQ/pyqpanda-algorithm` develop
`5f973efccb84bc193157d1ccebe32137e307293b`.

- Focused tests before the change: 7 passed, 4 failed. The four failures
detected 16 simulator calls where 10 suffice for four training samples.
- Focused tests after the change: 11 passed.
- Repository pytest run: 29 passed in 23.78 seconds.
- Kernel entries agree within absolute/relative tolerance `1e-12` with
independent NumPy state vectors constructed from RX, RY, and CZ matrices.
- Cases cover same-object inputs, equal copies, equal lists, duplicate
points, reordered samples, rectangular matrices, singletons, and empty
inputs. An SVR integration case compares against an independently
constructed precomputed kernel, allowing libsvm's stopping tolerance.

Test filenames follow the repository's `Test_*.py` discovery convention.
The prose contribution guide's dotted `feature.test.py` name cannot be
imported by pytest's current default import mode.

## Reproduce

From the repository root, with the package's dependencies plus pytest
installed (including pandas and scikit-learn used by existing imports):

```bash
PYTHONPATH=pyqpanda-algorithm MPLBACKEND=Agg python -m pytest \
-c test/pytest.ini -o addopts='' test/QAlgBase/Test_QSVR_kernel.py -q

PYTHONPATH=pyqpanda-algorithm MPLBACKEND=Agg python -m pytest \
-c test/pytest.ini -o addopts='' test -q

PYTHONPATH=pyqpanda-algorithm MPLBACKEND=Agg python \
benchmarks/benchmark_qsvr_kernel.py
```

`-o addopts=''` omits the optional Allure-reporting plugin flags.

## Local benchmark

Python 3.12.13, PyQPanda3 0.4.1, NumPy 2.3.5, Linux x86_64.
Three repetitions per size, fixed random seeds, median elapsed time,
alternating evaluation order after a simulator warm-up:

| Samples | Old calls | New calls | Old median | New median | Speedup |
|---:|---:|---:|---:|---:|---:|
| 16 | 256 | 136 | 12.90 ms | 6.74 ms | 1.91x |
| 32 | 1,024 | 528 | 50.71 ms | 26.17 ms | 1.94x |
| 64 | 4,096 | 2,080 | 244.51 ms | 116.94 ms | 2.09x |

Maximum absolute matrix difference: `1.1102230246251565e-15`.
These are local simulator measurements. They do not establish quantum
advantage or guarantee the same elapsed-time improvement on other systems.
12 changes: 11 additions & 1 deletion pyqpanda-algorithm/pyqpanda_alg/QSVR/QSVR.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,20 @@ def dist(self, x, y):
return re

def k_kernel(self, X, Y):
"""Build a fidelity kernel, reusing symmetry for equal sample arrays.

For X == Y, K[i, j] = K[j, i] because fidelity is symmetric. Only
n * (n + 1) / 2 simulator calls are needed instead of n ** 2.
Distinct sample arrays retain the full pairwise evaluation, even
when their shapes match. Diagonal values are still simulated.
"""
matrix = np.zeros((len(X), len(Y)))
symmetric = np.array_equal(X, Y)
for i in range(len(X)):
for j in range(len(Y)):
for j in range(i if symmetric else 0, len(Y)):
matrix[i][j] = self.dist(X[i], Y[j])
if symmetric:
matrix[j][i] = matrix[i][j]
return matrix

def get_res(self):
Expand Down
103 changes: 103 additions & 0 deletions test/QAlgBase/Test_QSVR_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Check kernel values independently and guard the simulator-call budget."""

import numpy as np
import pytest
from sklearn.svm import SVR

from pyqpanda_alg.QSVR import Quantum_SVR


def encoded_state(point):
"""Evaluate the two-qubit feature map with NumPy gate matrices only."""
def rx(angle):
c, s = np.cos(angle / 2), np.sin(angle / 2)
return np.array([[c, -1j * s], [-1j * s, c]])

def ry(angle):
c, s = np.cos(angle / 2), np.sin(angle / 2)
return np.array([[c, -s], [s, c]])

# q0 is the least significant bit. H on both qubits prepares |++>.
state = np.ones(4, dtype=complex) / 2
state = np.kron(ry(point[1]), ry(point[0])) @ state
state = np.diag([1, 1, 1, -1]) @ state
return np.kron(rx(point[0]), rx(point[1])) @ state


def reference_kernel(x, y):
result = np.empty((len(x), len(y)))
for i, first in enumerate(x):
for j, second in enumerate(y):
result[i, j] = abs(np.vdot(encoded_state(second),
encoded_state(first))) ** 2
return result


@pytest.fixture
def model():
x = np.array([[-0.9, 0.1], [0.3, -0.4], [1.2, 0.8], [-0.2, 1.7]])
return Quantum_SVR(x, np.array([0.1, -0.3, 1.4, 0.7]))


@pytest.mark.parametrize("case, expected_calls", [
("same_object", 10),
("equal_copy", 10),
("equal_lists", 10),
("duplicates", 10),
("permuted", 16),
("rectangular", 8),
("singleton", 1),
("empty_left", 0),
("empty_right", 0),
("empty_both", 0),
])
def test_qsvr_kernel_values_and_call_budget(model, monkeypatch, case, expected_calls):
x = model.x.copy()
if case == "same_object":
y = x
elif case == "equal_copy":
y = x.copy()
elif case == "equal_lists":
x, y = x.tolist(), x.tolist()
elif case == "duplicates":
x[1] = x[0]
y = x.copy()
elif case == "permuted":
y = x[::-1].copy()
elif case == "rectangular":
y = x[:2].copy()
elif case == "singleton":
x, y = x[:1], x[:1].copy()
elif case == "empty_left":
x, y = x[:0], x.copy()
elif case == "empty_right":
y = x[:0]
else:
x, y = x[:0], x[:0].copy()

calls = []
original_dist = model.dist

def measured_dist(first, second):
calls.append((first, second))
return original_dist(first, second)

monkeypatch.setattr(model, "dist", measured_dist)
actual = model.k_kernel(x, y)
np.testing.assert_allclose(actual, reference_kernel(x, y), atol=1e-12, rtol=1e-12)
assert actual.shape == (len(x), len(y))
assert len(calls) == expected_calls


def test_qsvr_predictions_match_independent_precomputed_kernel(model):
expected_model = SVR(kernel="precomputed", gamma=0.1)
kernel = reference_kernel(model.x, model.x)
expected_model.fit(kernel, model.y)

predicted, targets = model.get_res()

# Gate-level roundoff can change libsvm's last step within its stopping
# tolerance; matrix entries themselves are checked to 1e-12 above.
np.testing.assert_allclose(predicted, expected_model.predict(kernel),
atol=2 * expected_model.tol, rtol=0)
np.testing.assert_array_equal(targets, model.y)