Skip to content

【代码贡献】Fix QPCA crash on 4-feature input and k=2 returning 0 - #43

Open
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/qpca-broken-paths
Open

【代码贡献】Fix QPCA crash on 4-feature input and k=2 returning 0#43
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/qpca-broken-paths

Conversation

@mnn31

@mnn31 mnn31 commented Aug 9, 2026

Copy link
Copy Markdown

问题 / Problem

Three of the four qpca() entry points are broken. Only qpca(A, 1) on a
2-feature input, the case in the docstring and the demo notebook, works.

A = np.array([[-1, 2], [-2, -1], [-1, -2], [1, 3], [2, 1], [3, 2]])
qpca(A, 2)            # returns the literal int 0
qpca(A, 3)            # prints 'The K Error!' and kills the interpreter
qpca(np.random.randn(20, 4), 1)
# UnboundLocalError: cannot access local variable 'tao'

原因 / Root cause

  1. tao and qm are only assigned inside the A.shape[0] == 2 branch. The
    A.shape[0] == 4 branch assigns qm but never tao, so the next line
    raises UnboundLocalError. This is specific to 4 features: other feature
    counts fail earlier and more clearly at A.reshape(1, 2 ** n), since the
    covariance matrix has n² entries and the reshape asks for 2ⁿ, which agree
    only at n=2 and n=4.

    n=1  ValueError: cannot reshape array of size 1 into shape (1,2)
    n=3  ValueError: cannot reshape array of size 9 into shape (1,8)
    n=4  UnboundLocalError: cannot access local variable 'tao'
    n=5  ValueError: cannot reshape array of size 25 into shape (1,32)
    
  2. data = 0 is initialised and only overwritten inside if k == 1, so
    qpca(A, 2) falls through and returns the integer 0.

  3. Invalid k calls sys.exit(), which terminates the caller's process
    instead of raising.

  4. The k=1 projection vector takes np.sqrt twice. result_circuitA is
    already the amplitude vector, and on a non-degenerate spectrum the
    post-selection leaves two outcomes, so dividing by sum_A already makes it
    unit norm. Taking its square root again gives a vector of norm 2^(1/4) =
    1.1892, so every projected coordinate came out 18.9% too large.

I also checked the n=4 circuit code before deciding what to do with it. Wiring
tao/qm by hand is not enough to make it work:

  • the post-selected branch has probability 0. The n=4 program ends in
    |00000000> with probability 1, so the ancilla the algorithm post-selects on
    never flips and there is no output to read.
  • there is no n=4 result extraction at all. Everything after get_prob_dict
    sits inside if A.shape[0] == 2.
  • _init_cir ignores its state_vector argument, so nothing is amplitude
    encoded and the register the n=4 path would need is never populated.

Finishing that path means writing the state preparation and the extraction
step, which is a feature, not a bugfix. I left the n=4 circuit code in place
and gated the entry point instead.

修改 / Fix

  • Validate up front: a feature count other than 2 raises NotImplementedError
    with the count in the message, k outside {1, 2} raises ValueError. No
    more UnboundLocalError, no more sys.exit().
  • k=2 returns the actual PCA scores. tao = min(lambda) - 1 puts the
    threshold below every eigenvalue, and the circuit confirms this: the
    post-selection succeeds with probability 1, so the algorithm reports that
    nothing is filtered out. The retained subspace is therefore the whole
    feature space, and the basis for it is taken from the eigendecomposition the
    module already computes. Output is norm_x @ V.
  • Switched that decomposition from np.linalg.eig to np.linalg.eigh. The
    covariance matrix is symmetric by construction, so this guarantees real
    eigenvalues and an orthonormal basis, and the components are sorted
    descending. tao is unaffected, it only reads min, max and sum.
  • Removed the extra np.sqrt on the k=1 projection direction.
  • Dropped the dead result_idealA block, which was computed and then never
    read, and the now unused import sys.
  • Hardening, not a fix: post-selected keys are now read in sorted() order so
    component assignment does not lean on dict insertion order. On this circuit
    the two keys already came back sorted and at equal probability, so nothing
    observable changes.

数值验证 / Numerical validation

Compared against classical PCA, numpy eigendecomposition of the same
covariance matrix, on the docstring example. Eigenvalues are 30.4194 and
7.7472.

k=1, quantum output averaged over 20 runs (the circuit samples 8192 shots, so
it carries sampling noise of about 0.01 per coordinate):

row classical qpca after fix qpca before fix
1 -0.1373 -0.1144 -0.1361
2 -2.9500 -2.9455 -3.5028
3 -2.9344 -2.9482 -3.5061
4 1.9917 2.0054 2.3849
5 1.3080 1.2943 1.5392
6 2.7221 2.7085 3.2209

Max absolute error against classical drops from 0.5716 to 0.0229, which is
19.4% down to 0.78% of the largest coordinate. Correlation with the classical
projection is 0.99998. The recovered projection direction has norm 0.99997
against 1.1892 before, and sits 0.74 degrees off the classical principal
component.

k=2, previously the integer 0, now a (6, 2) array:

covariance of the output      [[30.4194, -1.4e-15],
                               [-1.4e-15,   7.7472]]
off-diagonal                  -1.4e-15          (was 11.3333)
column variances              30.4194, 7.7472   (was 19.3333, 18.8333)
eigenvalues, descending       30.4194, 7.7472

The scores are decorrelated and their column variances are the eigenvalues in
descending order, which is what distinguishes PCA output from the centred
input sitting in the original feature basis.

测试 / Tests

test/QPCA/Test_qpca.py, 8 tests, up from 1. The original test_qpca_normal
is untouched, the diff against develop is pure insertion. Added: k=1 against
classical PCA, k=1 projection direction is unit norm on the documented
example, k=2 decorrelation plus column variances plus per-column equality with
the classical scores, invalid k raises ValueError (parametrised over 0, 3,
-1), 4-feature input raises NotImplementedError for both k=1 and k=2.

python -m pytest test/QPCA -o addopts="" -q     # 8 passed
python -m pytest test      -o addopts="" -q     # 25 passed, was 18 on develop

Ran the QPCA file 10 times in a row to check the tolerances hold against shot
noise. The demo notebook
pyqpanda-algorithm/test/07-QPCA/demo01-QPCA-qpca.ipynb uses k=1 and still
runs.

已知限制(既有行为,本 PR 未改动) / Known limitation (pre-existing, unchanged by this PR)

This is existing upstream behaviour; this PR neither introduces nor changes
it. Flagging it so the numbers above are not read as more than they are.

The phase estimation gates are constants and _init_cir never encodes
state_vector, so the input matrix only reaches the circuit through the tao
branch comparison. The k=1 direction the circuit returns is therefore always
[1/sqrt(2), 1/sqrt(2)], which coincides with the principal component of the
docstring example but is not correct for general input. On
[[-3,3],[-2,2],[-1,1],[1,-1],[2,-2],[3,-3]], whose principal component is
[0.7071, -0.7071], classical PCA gives projections up to 4.24 while qpca
returns about 0.02.

The extraction is also sign-blind by construction: amplitudes are recovered as
square roots of measured probabilities, which are non-negative, so a principal
component with mixed signs can never be reproduced no matter what the circuit
does. Fixing this needs real state preparation and a data dependent evolution,
so I kept it out of this PR. Happy to open a separate one if that is wanted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant