Skip to content

【代码贡献】Fix SPSA optimizer ignoring parameter bounds - #41

Open
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/spsa-bounds
Open

【代码贡献】Fix SPSA optimizer ignoring parameter bounds#41
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/spsa-bounds

Conversation

@mnn31

@mnn31 mnn31 commented Aug 9, 2026

Copy link
Copy Markdown

Relates to #13.

Problem

spsa_minimize silently ignores the bounds argument in the normal case, and
corrupts it in the other case. The first path is what QAOA.run(optimizer='SPSA')
hits, since it always passes one pair per parameter; the second is reachable
through the documented spsa_minimize(..., bounds=[(min, max)]) public API.

Root cause

pyqpanda_alg/QAOA/spsa.py, _check_bounds (lines 17-33).

  1. One (min, max) pair per variable, len(bounds) == d: the branch validates
    the dimension and then falls off the end of the function, so it returns
    None. _jail_inside reads None as "no bounds" and never clips.
  2. One pair for all variables, len(bounds) == 1: return bounds * d
    multiplies the numpy array element-wise by d instead of repeating the row
    d times. _check_bounds(3, [(0, 1)]) gives [[0, 3]], which is the wrong
    interval and the wrong shape, so the later np.clip broadcasts one bogus
    pair over every variable.

Fix

Return the validated array in the per-variable branch, and use np.tile for
the single-pair branch. The empty-bounds warning and both existing
ValueError / IndexError checks are unchanged.

Verification

import numpy as np
from pyqpanda_alg.QAOA.spsa import spsa_minimize, _check_bounds

np.random.seed(0)
f = lambda x: float(np.sum(x ** 2) - np.sum(x))   # unconstrained min at x = 0.5
print(spsa_minimize(f, np.array([5.0, 5.0]), bounds=[(1, 2), (1, 2)], maxiter=200))
print(_check_bounds(3, [(0, 1)]))

before

[0.5 0.5]                  <- outside [1, 2]
[[0 3]]

after

[1. 1.]
[[0 1]
 [0 1]
 [0 1]]

Same thing through QAOA.run(optimizer='SPSA') with
gamma_bounds = beta_bounds = [(0.0, 0.5)]: on develop the returned
parameters come back outside the bounds; with this change they stay inside.
With the default (unbounded) settings the result is bit-identical to develop.

Tests

5 cases added to test/QAOA/Test_spsa_minimize.py: bounds respected end to end
for per-variable bounds and for a single tiled pair, _check_bounds returning
the array instead of None, single-pair tiling, and the existing validation
errors. Seeded, deterministic. 4 of the 5 fail on develop and pass with this
change.

pytest test/QAOA/Test_spsa_minimize.py     6 passed
cd test && pytest                          22 passed

The gradient line in spsa_minimize divides by xp - xm rather than
2 * ck * delta. With delta in {-1, 1} and no clipping these are
identical, and when clipping is active the current form is a finite difference
over the points actually evaluated, so I left it alone. Degenerate
lo == hi bounds make that denominator zero; that is a property of the
existing gradient line, not of this change, so it is out of scope here too.

_check_bounds returned None when one (min, max) pair was given per
variable, so _jail_inside treated it as no bounds and never clipped.
The single-pair branch multiplied the bounds array by d instead of
repeating the pair d times.
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