softopt 0.6.0__tar.gz → 0.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {softopt-0.6.0/softopt.egg-info → softopt-0.6.2}/PKG-INFO +37 -2
- {softopt-0.6.0 → softopt-0.6.2}/README.md +36 -1
- {softopt-0.6.0 → softopt-0.6.2}/pyproject.toml +1 -1
- {softopt-0.6.0 → softopt-0.6.2}/softopt/__init__.py +5 -4
- {softopt-0.6.0 → softopt-0.6.2}/softopt/numpy_optimizer.py +61 -9
- {softopt-0.6.0 → softopt-0.6.2/softopt.egg-info}/PKG-INFO +37 -2
- {softopt-0.6.0 → softopt-0.6.2}/softopt.egg-info/SOURCES.txt +1 -0
- softopt-0.6.2/tests/test_measured_slope.py +77 -0
- {softopt-0.6.0 → softopt-0.6.2}/LICENSE +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/setup.cfg +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt/compile.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt/soft_number.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt/torch_optimizer.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt.egg-info/dependency_links.txt +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt.egg-info/requires.txt +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/softopt.egg-info/top_level.txt +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/tests/test_basic.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/tests/test_causal.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/tests/test_compile.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/tests/test_soft_number.py +0 -0
- {softopt-0.6.0 → softopt-0.6.2}/tests/test_torch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: softopt
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
5
|
Author: Ido Angel
|
|
6
6
|
License: MIT
|
|
@@ -85,7 +85,7 @@ is the mechanism.
|
|
|
85
85
|
But at 10 qubits both arms covered only a few percent of the way to the ground
|
|
86
86
|
state. 1.5× of very little is still very little. **If the plateau is your
|
|
87
87
|
problem, this is not the fix.** The scan and its replication are in
|
|
88
|
-
`
|
|
88
|
+
`benchmarks_barren_plateau/`.
|
|
89
89
|
|
|
90
90
|
## Installation
|
|
91
91
|
|
|
@@ -122,6 +122,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
|
|
|
122
122
|
|
|
123
123
|
If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
|
|
124
124
|
|
|
125
|
+
## Where the slope comes from: `slope="model"` or `slope="measured"`
|
|
126
|
+
|
|
127
|
+
Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
|
|
128
|
+
|
|
129
|
+
| | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| slope from | your model's exact derivative | your readings, along the direction you probed |
|
|
132
|
+
| converges to | the **model's** optimum | the **real system's** optimum |
|
|
133
|
+
| choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
|
|
134
|
+
|
|
135
|
+
With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from softopt import SoftOpt, soft_compile, spsa_gradient
|
|
139
|
+
|
|
140
|
+
opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
|
|
141
|
+
|
|
142
|
+
for step in range(num_steps):
|
|
143
|
+
grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
|
|
144
|
+
theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
|
|
148
|
+
|
|
149
|
+
If your model is exact, you do not need either mode, or any readings: optimise the model directly.
|
|
150
|
+
|
|
125
151
|
## PyTorch
|
|
126
152
|
|
|
127
153
|
If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
|
|
@@ -169,6 +195,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
169
195
|
| BeH₂ | 94.7% | 5/5 |
|
|
170
196
|
| HeH⁺ | 90.9% | 5/5 |
|
|
171
197
|
|
|
198
|
+
**Quantum chemistry on a real molecule, and against COBYLA**
|
|
199
|
+
|
|
200
|
+
| Test | Baseline | Result | Win rate |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
|
|
203
|
+
| H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
|
|
204
|
+
|
|
205
|
+
Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
|
|
206
|
+
|
|
172
207
|
**Quantum control**
|
|
173
208
|
|
|
174
209
|
| Domain | Improvement | Win rate |
|
|
@@ -59,7 +59,7 @@ is the mechanism.
|
|
|
59
59
|
But at 10 qubits both arms covered only a few percent of the way to the ground
|
|
60
60
|
state. 1.5× of very little is still very little. **If the plateau is your
|
|
61
61
|
problem, this is not the fix.** The scan and its replication are in
|
|
62
|
-
`
|
|
62
|
+
`benchmarks_barren_plateau/`.
|
|
63
63
|
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
@@ -96,6 +96,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
|
|
|
96
96
|
|
|
97
97
|
If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
|
|
98
98
|
|
|
99
|
+
## Where the slope comes from: `slope="model"` or `slope="measured"`
|
|
100
|
+
|
|
101
|
+
Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
|
|
102
|
+
|
|
103
|
+
| | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| slope from | your model's exact derivative | your readings, along the direction you probed |
|
|
106
|
+
| converges to | the **model's** optimum | the **real system's** optimum |
|
|
107
|
+
| choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
|
|
108
|
+
|
|
109
|
+
With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from softopt import SoftOpt, soft_compile, spsa_gradient
|
|
113
|
+
|
|
114
|
+
opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
|
|
115
|
+
|
|
116
|
+
for step in range(num_steps):
|
|
117
|
+
grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
|
|
118
|
+
theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
|
|
122
|
+
|
|
123
|
+
If your model is exact, you do not need either mode, or any readings: optimise the model directly.
|
|
124
|
+
|
|
99
125
|
## PyTorch
|
|
100
126
|
|
|
101
127
|
If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
|
|
@@ -143,6 +169,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
143
169
|
| BeH₂ | 94.7% | 5/5 |
|
|
144
170
|
| HeH⁺ | 90.9% | 5/5 |
|
|
145
171
|
|
|
172
|
+
**Quantum chemistry on a real molecule, and against COBYLA**
|
|
173
|
+
|
|
174
|
+
| Test | Baseline | Result | Win rate |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
|
|
177
|
+
| H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
|
|
178
|
+
|
|
179
|
+
Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
|
|
180
|
+
|
|
146
181
|
**Quantum control**
|
|
147
182
|
|
|
148
183
|
| Domain | Improvement | Win rate |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "softopt"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -3,7 +3,8 @@ SoftOpt — a standalone optimizer built on Klein–Maimon soft-number
|
|
|
3
3
|
calculus, for problems with a known, differentiable computation graph
|
|
4
4
|
(quantum circuits, physical models, projection models, ...).
|
|
5
5
|
|
|
6
|
-
from softopt import SoftOpt # numpy version
|
|
6
|
+
from softopt import SoftOpt # numpy version (slope="model" or "measured")
|
|
7
|
+
from softopt import spsa_gradient # gradient, direction and measured slope from 2 readings
|
|
7
8
|
from softopt import SoftOptTorch # PyTorch version (torch.optim.Optimizer)
|
|
8
9
|
from softopt import soft_compile # turn a plain-Python model into g_delta
|
|
9
10
|
from softopt import SoftNumber # the algebraic primitive itself
|
|
@@ -11,7 +12,7 @@ calculus, for problems with a known, differentiable computation graph
|
|
|
11
12
|
See README.md for the exact requirement your problem must satisfy
|
|
12
13
|
(the "known computation graph" criterion) and worked examples.
|
|
13
14
|
"""
|
|
14
|
-
from .numpy_optimizer import SoftOpt
|
|
15
|
+
from .numpy_optimizer import SoftOpt, spsa_gradient
|
|
15
16
|
from .compile import soft_compile
|
|
16
17
|
from .soft_number import (
|
|
17
18
|
SoftNumber, sadd, sneg, ssub, smul, sinv, sdiv,
|
|
@@ -28,8 +29,8 @@ def __getattr__(name):
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
31
|
-
"SoftOpt", "SoftOptTorch", "SoftNumber", "soft_compile",
|
|
32
|
+
"SoftOpt", "SoftOptTorch", "SoftNumber", "soft_compile", "spsa_gradient",
|
|
32
33
|
"sadd", "sneg", "ssub", "smul", "sinv", "sdiv",
|
|
33
34
|
"spow", "sroot", "ssqrt", "ssin", "scos", "sexp", "slog",
|
|
34
35
|
]
|
|
35
|
-
__version__ = "0.6.
|
|
36
|
+
__version__ = "0.6.2"
|
|
@@ -7,10 +7,25 @@ derivative of your true objective along delta, computed from your own
|
|
|
7
7
|
known model (circuit, projection, physical law). `soft_compile` generates
|
|
8
8
|
this for you from a model written in ordinary Python.
|
|
9
9
|
|
|
10
|
-
The correction is a bounded Newton step
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
the
|
|
10
|
+
The correction is a bounded Newton step, t* = -D1 / D2 along a direction delta.
|
|
11
|
+
D2 (the curvature) always comes from your model: a central finite difference
|
|
12
|
+
OF the exact soft-number derivative, with the usual step-size trade-off,
|
|
13
|
+
controlled by h_fd. D1 (the slope) has two possible sources, set by `slope`:
|
|
14
|
+
|
|
15
|
+
slope="model" (default) D1 is the model's exact directional derivative
|
|
16
|
+
along a fresh random direction. Robust to very noisy
|
|
17
|
+
readings, but the correction pulls towards the MODEL's
|
|
18
|
+
optimum: if the model is biased, so is the answer.
|
|
19
|
+
|
|
20
|
+
slope="measured" D1 is the slope you measured on the real system, along the
|
|
21
|
+
direction you measured it in (an SPSA difference gives you
|
|
22
|
+
both for free). The model only supplies the curvature, so
|
|
23
|
+
the fixed point is where the MEASURED slope vanishes: the
|
|
24
|
+
real system's optimum, even when the model is off. Needs
|
|
25
|
+
readings clean enough that the measured slope is informative.
|
|
26
|
+
|
|
27
|
+
`spsa_gradient` returns the gradient estimate, the direction and the measured
|
|
28
|
+
slope in one call, ready for either mode.
|
|
14
29
|
"""
|
|
15
30
|
import numpy as np
|
|
16
31
|
|
|
@@ -18,7 +33,10 @@ import numpy as np
|
|
|
18
33
|
class SoftOpt:
|
|
19
34
|
def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
|
|
20
35
|
newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
|
|
21
|
-
h_fd=1e-4, seed=None, _test_frozen_constant=1.0):
|
|
36
|
+
h_fd=1e-4, seed=None, slope="model", _test_frozen_constant=1.0):
|
|
37
|
+
if slope not in ("model", "measured"):
|
|
38
|
+
raise ValueError(f'slope must be "model" or "measured", got {slope!r}')
|
|
39
|
+
self.slope = slope
|
|
22
40
|
self.g_delta = g_delta
|
|
23
41
|
self.lr = lr
|
|
24
42
|
self.b1, self.b2 = betas
|
|
@@ -34,13 +52,19 @@ class SoftOpt:
|
|
|
34
52
|
self._ablation_rng = None
|
|
35
53
|
self._test_frozen_constant = _test_frozen_constant
|
|
36
54
|
|
|
37
|
-
def step(self, theta, grad_estimate,
|
|
55
|
+
def step(self, theta, grad_estimate, direction=None, measured_slope=None,
|
|
56
|
+
_test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
|
|
38
57
|
"""One complete optimizer step: Adam base update + Newton-style
|
|
39
58
|
correction from the known model, in a single call.
|
|
40
59
|
theta: current parameters (np.ndarray)
|
|
41
60
|
grad_estimate: a gradient estimate of the loss w.r.t. theta (e.g.
|
|
42
61
|
from SPSA or backprop) -- exactly what you'd normally hand
|
|
43
62
|
to Adam.
|
|
63
|
+
direction, measured_slope: required when slope="measured", ignored
|
|
64
|
+
otherwise. The direction your readings probed and the slope you
|
|
65
|
+
measured along it at theta, e.g. for SPSA
|
|
66
|
+
(f(theta + c*d) - f(theta - c*d)) / (2c) along d.
|
|
67
|
+
spsa_gradient() returns both.
|
|
44
68
|
Returns: theta for the next step.
|
|
45
69
|
|
|
46
70
|
The _test_* arguments are TEST-ONLY hooks used exclusively by the
|
|
@@ -53,6 +77,13 @@ class SoftOpt:
|
|
|
53
77
|
"""
|
|
54
78
|
theta = np.asarray(theta, dtype=float)
|
|
55
79
|
grad_estimate = np.asarray(grad_estimate, dtype=float)
|
|
80
|
+
if self.slope == "measured":
|
|
81
|
+
if direction is None or measured_slope is None:
|
|
82
|
+
raise ValueError('slope="measured" needs direction= and measured_slope= on every '
|
|
83
|
+
'step (spsa_gradient() returns both)')
|
|
84
|
+
direction = np.asarray(direction, dtype=float)
|
|
85
|
+
if direction.shape != theta.shape:
|
|
86
|
+
raise ValueError(f"direction has shape {direction.shape}, theta has {theta.shape}")
|
|
56
87
|
|
|
57
88
|
# --- Adam base update ---
|
|
58
89
|
self.t += 1
|
|
@@ -70,8 +101,11 @@ class SoftOpt:
|
|
|
70
101
|
|
|
71
102
|
# --- correction from the known, exact model ---
|
|
72
103
|
n = theta.shape[0]
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
if self.slope == "measured":
|
|
105
|
+
delta = direction
|
|
106
|
+
else:
|
|
107
|
+
gen_rng = _test_rng if _test_rng is not None else self.rng
|
|
108
|
+
delta = gen_rng.choice([-1.0, 1.0], size=n)
|
|
75
109
|
|
|
76
110
|
# The ablation arms must not disturb the probe-direction stream, or
|
|
77
111
|
# a corrupted arm would also change every direction that follows and
|
|
@@ -79,7 +113,10 @@ class SoftOpt:
|
|
|
79
113
|
if self._ablation_rng is None:
|
|
80
114
|
self._ablation_rng = np.random.default_rng(
|
|
81
115
|
self.rng.integers(0, 2**63 - 1))
|
|
82
|
-
|
|
116
|
+
if self.slope == "measured":
|
|
117
|
+
D1 = float(measured_slope) # from the real system
|
|
118
|
+
else:
|
|
119
|
+
D1 = self.g_delta(theta_after_adam, delta)
|
|
83
120
|
gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
|
|
84
121
|
gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
|
|
85
122
|
D2_real = (gp - gm) / (2 * self.h_fd)
|
|
@@ -104,3 +141,18 @@ class SoftOpt:
|
|
|
104
141
|
t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
|
|
105
142
|
|
|
106
143
|
return theta_after_adam + t_star * delta
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def spsa_gradient(measure, theta, c=0.1, rng=None):
|
|
147
|
+
"""One SPSA probe of a noisy objective.
|
|
148
|
+
|
|
149
|
+
measure: callable theta -> one (noisy) reading of the real system.
|
|
150
|
+
Returns (grad_estimate, direction, measured_slope), where measured_slope
|
|
151
|
+
is (measure(theta + c*d) - measure(theta - c*d)) / (2c) along the random
|
|
152
|
+
+-1 direction d, and grad_estimate = measured_slope * d. Costs 2 readings.
|
|
153
|
+
"""
|
|
154
|
+
theta = np.asarray(theta, dtype=float)
|
|
155
|
+
rng = np.random.default_rng() if rng is None else rng
|
|
156
|
+
d = rng.choice([-1.0, 1.0], size=theta.shape)
|
|
157
|
+
slope = (measure(theta + c * d) - measure(theta - c * d)) / (2 * c)
|
|
158
|
+
return slope * d, d, float(slope)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: softopt
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
5
|
Author: Ido Angel
|
|
6
6
|
License: MIT
|
|
@@ -85,7 +85,7 @@ is the mechanism.
|
|
|
85
85
|
But at 10 qubits both arms covered only a few percent of the way to the ground
|
|
86
86
|
state. 1.5× of very little is still very little. **If the plateau is your
|
|
87
87
|
problem, this is not the fix.** The scan and its replication are in
|
|
88
|
-
`
|
|
88
|
+
`benchmarks_barren_plateau/`.
|
|
89
89
|
|
|
90
90
|
## Installation
|
|
91
91
|
|
|
@@ -122,6 +122,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
|
|
|
122
122
|
|
|
123
123
|
If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
|
|
124
124
|
|
|
125
|
+
## Where the slope comes from: `slope="model"` or `slope="measured"`
|
|
126
|
+
|
|
127
|
+
Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
|
|
128
|
+
|
|
129
|
+
| | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| slope from | your model's exact derivative | your readings, along the direction you probed |
|
|
132
|
+
| converges to | the **model's** optimum | the **real system's** optimum |
|
|
133
|
+
| choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
|
|
134
|
+
|
|
135
|
+
With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from softopt import SoftOpt, soft_compile, spsa_gradient
|
|
139
|
+
|
|
140
|
+
opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
|
|
141
|
+
|
|
142
|
+
for step in range(num_steps):
|
|
143
|
+
grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
|
|
144
|
+
theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
|
|
148
|
+
|
|
149
|
+
If your model is exact, you do not need either mode, or any readings: optimise the model directly.
|
|
150
|
+
|
|
125
151
|
## PyTorch
|
|
126
152
|
|
|
127
153
|
If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
|
|
@@ -169,6 +195,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
169
195
|
| BeH₂ | 94.7% | 5/5 |
|
|
170
196
|
| HeH⁺ | 90.9% | 5/5 |
|
|
171
197
|
|
|
198
|
+
**Quantum chemistry on a real molecule, and against COBYLA**
|
|
199
|
+
|
|
200
|
+
| Test | Baseline | Result | Win rate |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
|
|
203
|
+
| H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
|
|
204
|
+
|
|
205
|
+
Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
|
|
206
|
+
|
|
172
207
|
**Quantum control**
|
|
173
208
|
|
|
174
209
|
| Domain | Improvement | Win rate |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""slope="measured": slope from the real system, curvature from the model."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
from softopt import SoftOpt, soft_compile, spsa_gradient
|
|
5
|
+
|
|
6
|
+
N = 6
|
|
7
|
+
SHIFT = np.array([0.8, -0.6, 0.5, -0.4, 0.7, -0.5]) # the model's optimum is off by this
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def truth(x):
|
|
11
|
+
return float(np.sum(np.asarray(x) ** 2))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def biased_model(th):
|
|
15
|
+
return sum((t - s) ** 2 for t, s in zip(th, SHIFT))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run(slope, steps=400, seed=0):
|
|
19
|
+
g_delta = soft_compile(biased_model, n_params=N)
|
|
20
|
+
opt = SoftOpt(N, g_delta, lr=0.05, seed=seed, slope=slope)
|
|
21
|
+
rng = np.random.default_rng(seed)
|
|
22
|
+
x = np.full(N, 2.0)
|
|
23
|
+
for _ in range(steps):
|
|
24
|
+
g, d, s = spsa_gradient(truth, x, c=0.1, rng=rng)
|
|
25
|
+
x = opt.step(x, g, direction=d, measured_slope=s)
|
|
26
|
+
return x
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_measured_slope_finds_the_real_optimum_despite_a_biased_model():
|
|
30
|
+
x = _run("measured")
|
|
31
|
+
assert np.allclose(x, 0.0, atol=0.05) # the system's optimum
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_model_slope_follows_the_model():
|
|
35
|
+
x = _run("model")
|
|
36
|
+
# documented behaviour: the correction pulls towards the model's optimum
|
|
37
|
+
assert np.linalg.norm(x - SHIFT) < np.linalg.norm(x - 0.0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_measured_mode_requires_direction_and_slope():
|
|
41
|
+
opt = SoftOpt(N, soft_compile(biased_model, n_params=N), slope="measured")
|
|
42
|
+
with pytest.raises(ValueError, match="direction"):
|
|
43
|
+
opt.step(np.ones(N), np.ones(N))
|
|
44
|
+
with pytest.raises(ValueError, match="shape"):
|
|
45
|
+
opt.step(np.ones(N), np.ones(N), direction=np.ones(N + 1), measured_slope=1.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_unknown_slope_mode_rejected():
|
|
49
|
+
with pytest.raises(ValueError, match="slope"):
|
|
50
|
+
SoftOpt(N, lambda t, d: 0.0, slope="both")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_model_mode_ignores_direction_arguments():
|
|
54
|
+
g_delta = soft_compile(biased_model, n_params=N)
|
|
55
|
+
a = SoftOpt(N, g_delta, lr=0.05, seed=3)
|
|
56
|
+
b = SoftOpt(N, g_delta, lr=0.05, seed=3)
|
|
57
|
+
x = np.ones(N)
|
|
58
|
+
xa = a.step(x, 2 * x)
|
|
59
|
+
xb = b.step(x, 2 * x, direction=-np.ones(N), measured_slope=123.0)
|
|
60
|
+
assert np.array_equal(xa, xb)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_spsa_gradient_on_a_linear_function_is_exact():
|
|
64
|
+
w = np.arange(1.0, N + 1)
|
|
65
|
+
g, d, s = spsa_gradient(lambda x: float(w @ x), np.zeros(N), c=0.3, rng=np.random.default_rng(0))
|
|
66
|
+
assert s == pytest.approx(w @ d)
|
|
67
|
+
assert np.allclose(g, s * d)
|
|
68
|
+
assert set(np.unique(d)) <= {-1.0, 1.0}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_frozen_ablation_hook_works_in_measured_mode():
|
|
72
|
+
g_delta = soft_compile(biased_model, n_params=N)
|
|
73
|
+
opt = SoftOpt(N, g_delta, lr=0.05, seed=0, slope="measured")
|
|
74
|
+
x = np.ones(N)
|
|
75
|
+
g, d, s = spsa_gradient(truth, x, rng=np.random.default_rng(1))
|
|
76
|
+
out = opt.step(x, g, direction=d, measured_slope=s, _test_magnitude_source="frozen")
|
|
77
|
+
assert out.shape == x.shape and np.all(np.isfinite(out))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|