softopt 0.1.0__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.1.0/LICENSE +21 -0
- softopt-0.1.0/PKG-INFO +162 -0
- softopt-0.1.0/README.md +135 -0
- softopt-0.1.0/pyproject.toml +38 -0
- softopt-0.1.0/setup.cfg +4 -0
- softopt-0.1.0/softopt/__init__.py +22 -0
- softopt-0.1.0/softopt/numpy_optimizer.py +168 -0
- softopt-0.1.0/softopt/torch_optimizer.py +150 -0
- softopt-0.1.0/softopt.egg-info/PKG-INFO +162 -0
- softopt-0.1.0/softopt.egg-info/SOURCES.txt +12 -0
- softopt-0.1.0/softopt.egg-info/dependency_links.txt +1 -0
- softopt-0.1.0/softopt.egg-info/requires.txt +8 -0
- softopt-0.1.0/softopt.egg-info/top_level.txt +1 -0
- softopt-0.1.0/tests/test_basic.py +64 -0
softopt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ido Angel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
softopt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: softopt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
|
+
Author: Ido Angel
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mobiuai/softopt
|
|
8
|
+
Project-URL: Repository, https://github.com/mobiuai/softopt
|
|
9
|
+
Project-URL: Issues, https://github.com/mobiuai/softopt/issues
|
|
10
|
+
Keywords: optimizer,optimization,soft-logic,soft-numbers,quantum-computing,vqe,qaoa,pytorch
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.20
|
|
21
|
+
Provides-Extra: torch
|
|
22
|
+
Requires-Dist: torch>=2.0; extra == "torch"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: torch>=2.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# SoftOpt
|
|
29
|
+
|
|
30
|
+
**A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
|
|
31
|
+
|
|
32
|
+
SoftOpt is free, fully local, and requires no server, license key, or network access — the same way `torch.optim.Adam` requires none. It is not a wrapper around Adam or any other optimizer: it is a complete, drop-in optimizer with its own Adam-equivalent base update, plus an exact Newton-style correction built on Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024).
|
|
33
|
+
|
|
34
|
+
## Where it helps
|
|
35
|
+
|
|
36
|
+
If your problem has a **known computation graph** — a quantum circuit, a physical simulator, a projection or measurement model, anything you can write down exactly, even if the *measurements* of it are noisy — SoftOpt computes an exact directional derivative and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
|
|
37
|
+
|
|
38
|
+
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
39
|
+
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
40
|
+
- **Computer vision** — multi-camera bundle adjustment / camera calibration
|
|
41
|
+
- **Finance** — portfolio optimization (Markowitz mean-variance)
|
|
42
|
+
- **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
|
|
43
|
+
- Pharmacokinetics, logistic regression, and more
|
|
44
|
+
|
|
45
|
+
## Where it does *not* help
|
|
46
|
+
|
|
47
|
+
Full reinforcement learning (or anything else with a continuously **moving target** — a policy, an adversary, a non-stationary distribution) is outside SoftOpt's validated scope. The mechanism needs a *fixed* objective to compute a meaningful correction against; a moving target breaks that assumption. Use plain Adam/SGD there.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install softopt # numpy version only
|
|
53
|
+
pip install softopt[torch] # + the PyTorch optimizer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
### NumPy
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from softopt import SoftOpt
|
|
62
|
+
|
|
63
|
+
# g_delta(theta, delta) must return the EXACT directional derivative of
|
|
64
|
+
# your true, differentiable objective along `delta`, at `theta` — computed
|
|
65
|
+
# from your own known model (a circuit, a projection, a physical law),
|
|
66
|
+
# not estimated from noisy measurements.
|
|
67
|
+
opt = SoftOpt(n_params, g_delta, lr=0.02)
|
|
68
|
+
|
|
69
|
+
for step in range(num_steps):
|
|
70
|
+
grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
|
|
71
|
+
theta = opt.step(theta, grad_estimate)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### PyTorch
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from softopt import SoftOptTorch
|
|
78
|
+
|
|
79
|
+
opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
|
|
80
|
+
|
|
81
|
+
for batch in data:
|
|
82
|
+
loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
|
|
86
|
+
|
|
87
|
+
## Two correction modes
|
|
88
|
+
|
|
89
|
+
SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
|
|
90
|
+
|
|
91
|
+
- **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
|
|
92
|
+
- **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
|
|
93
|
+
|
|
94
|
+
**How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
|
|
95
|
+
|
|
96
|
+
## Validated results
|
|
97
|
+
|
|
98
|
+
All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
|
|
99
|
+
|
|
100
|
+
**Quantum chemistry (VQE)**
|
|
101
|
+
|
|
102
|
+
| Domain | Improvement | Win rate |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| H₂ | 66.6% | 5/5 |
|
|
105
|
+
| H₄ | 89.8% | 5/5 |
|
|
106
|
+
| C₁₃Cl₂ (13-term Hamiltonian, incl. a 4-body term) | 81.3% | 5/5 |
|
|
107
|
+
| BeH₂ | 94.7% | 5/5 |
|
|
108
|
+
| HeH⁺ | 90.9% | 5/5 |
|
|
109
|
+
|
|
110
|
+
**Quantum control**
|
|
111
|
+
|
|
112
|
+
| Domain | Improvement | Win rate |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| GRAPE (2-qubit) | 84.0% | 20/20 |
|
|
115
|
+
|
|
116
|
+
**Condensed-matter & spin models**
|
|
117
|
+
|
|
118
|
+
| Domain | Improvement | Win rate |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| Ferromagnetic Ising (6-qubit chain) | 82.1% | 5/5 |
|
|
121
|
+
| Transverse Ising (6-qubit chain) | 71.2% | 5/5 |
|
|
122
|
+
| XY model (6-qubit chain) | 62.3% | 5/5 |
|
|
123
|
+
| Antiferromagnetic Heisenberg (6-qubit chain) | 61.3% | 5/5 |
|
|
124
|
+
| SSH model (topological, 6-qubit chain) | 71.7% | 5/5 |
|
|
125
|
+
| Kitaev chain (6-qubit) | 68.8% | 5/5 |
|
|
126
|
+
| Fibonacci chain (classical antiferromagnetic XY, N=16) | 95.4% | 10/10 |
|
|
127
|
+
| Penrose quasicrystal XY-model (50 sites) | 146.4% | 10/10 |
|
|
128
|
+
|
|
129
|
+
**Computer vision**
|
|
130
|
+
|
|
131
|
+
| Domain | Improvement | Win rate |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 93.9% | 17/20 |
|
|
134
|
+
|
|
135
|
+
**Finance**
|
|
136
|
+
|
|
137
|
+
| Domain | Improvement | Win rate |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| Portfolio optimization (realistic backtest noise) | ~58x lower loss | 20/20 |
|
|
140
|
+
|
|
141
|
+
See `benchmarks/` for the exact, runnable scripts behind every one of these numbers, including the raw per-seed results.
|
|
142
|
+
|
|
143
|
+
## The math
|
|
144
|
+
|
|
145
|
+
SoftOpt's exact-derivative computation is a direct implementation of specific results from *Foundations of Soft Logic* (Klein & Maimon, Springer 2024):
|
|
146
|
+
|
|
147
|
+
| Operation | Book source | Formula |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `sadd(a,b)` | §4.3.1, p.27 | `(a+c, b+d)` |
|
|
150
|
+
| `smul(a,b)` | §4.3.1, p.27 | `(ad+bc, bd)` |
|
|
151
|
+
| `ssin`, `scos`, `sexp` | Lemma 6.1 (p.40) & §6.2 (p.41) | `f(a,b) = (a·f′(b), f(b))` |
|
|
152
|
+
| `sinv(a,b)` | Lemma 6.2(d), p.42 | `(−a/b², 1/b)` |
|
|
153
|
+
| `sdiv(x,y)` | composition | `smul(x, sinv(y))` |
|
|
154
|
+
|
|
155
|
+
## What SoftOpt is *not*
|
|
156
|
+
|
|
157
|
+
- Not a claim to beat specialized full-Jacobian second-order solvers (Levenberg–Marquardt, L-BFGS) where those are already practical — SoftOpt's validated niche is genuine improvement over first-order optimizers (Adam, SGD) already in use, particularly where switching to a full second-order method isn't practical (embedded in a larger pipeline, high dimensionality, or measurement noise).
|
|
158
|
+
- Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT. See [LICENSE](LICENSE).
|
softopt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SoftOpt
|
|
2
|
+
|
|
3
|
+
**A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
|
|
4
|
+
|
|
5
|
+
SoftOpt is free, fully local, and requires no server, license key, or network access — the same way `torch.optim.Adam` requires none. It is not a wrapper around Adam or any other optimizer: it is a complete, drop-in optimizer with its own Adam-equivalent base update, plus an exact Newton-style correction built on Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024).
|
|
6
|
+
|
|
7
|
+
## Where it helps
|
|
8
|
+
|
|
9
|
+
If your problem has a **known computation graph** — a quantum circuit, a physical simulator, a projection or measurement model, anything you can write down exactly, even if the *measurements* of it are noisy — SoftOpt computes an exact directional derivative and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
|
|
10
|
+
|
|
11
|
+
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
12
|
+
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
13
|
+
- **Computer vision** — multi-camera bundle adjustment / camera calibration
|
|
14
|
+
- **Finance** — portfolio optimization (Markowitz mean-variance)
|
|
15
|
+
- **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
|
|
16
|
+
- Pharmacokinetics, logistic regression, and more
|
|
17
|
+
|
|
18
|
+
## Where it does *not* help
|
|
19
|
+
|
|
20
|
+
Full reinforcement learning (or anything else with a continuously **moving target** — a policy, an adversary, a non-stationary distribution) is outside SoftOpt's validated scope. The mechanism needs a *fixed* objective to compute a meaningful correction against; a moving target breaks that assumption. Use plain Adam/SGD there.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install softopt # numpy version only
|
|
26
|
+
pip install softopt[torch] # + the PyTorch optimizer
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
### NumPy
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from softopt import SoftOpt
|
|
35
|
+
|
|
36
|
+
# g_delta(theta, delta) must return the EXACT directional derivative of
|
|
37
|
+
# your true, differentiable objective along `delta`, at `theta` — computed
|
|
38
|
+
# from your own known model (a circuit, a projection, a physical law),
|
|
39
|
+
# not estimated from noisy measurements.
|
|
40
|
+
opt = SoftOpt(n_params, g_delta, lr=0.02)
|
|
41
|
+
|
|
42
|
+
for step in range(num_steps):
|
|
43
|
+
grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
|
|
44
|
+
theta = opt.step(theta, grad_estimate)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### PyTorch
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from softopt import SoftOptTorch
|
|
51
|
+
|
|
52
|
+
opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
|
|
53
|
+
|
|
54
|
+
for batch in data:
|
|
55
|
+
loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
|
|
59
|
+
|
|
60
|
+
## Two correction modes
|
|
61
|
+
|
|
62
|
+
SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
|
|
63
|
+
|
|
64
|
+
- **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
|
|
65
|
+
- **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
|
|
66
|
+
|
|
67
|
+
**How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
|
|
68
|
+
|
|
69
|
+
## Validated results
|
|
70
|
+
|
|
71
|
+
All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
|
|
72
|
+
|
|
73
|
+
**Quantum chemistry (VQE)**
|
|
74
|
+
|
|
75
|
+
| Domain | Improvement | Win rate |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| H₂ | 66.6% | 5/5 |
|
|
78
|
+
| H₄ | 89.8% | 5/5 |
|
|
79
|
+
| C₁₃Cl₂ (13-term Hamiltonian, incl. a 4-body term) | 81.3% | 5/5 |
|
|
80
|
+
| BeH₂ | 94.7% | 5/5 |
|
|
81
|
+
| HeH⁺ | 90.9% | 5/5 |
|
|
82
|
+
|
|
83
|
+
**Quantum control**
|
|
84
|
+
|
|
85
|
+
| Domain | Improvement | Win rate |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| GRAPE (2-qubit) | 84.0% | 20/20 |
|
|
88
|
+
|
|
89
|
+
**Condensed-matter & spin models**
|
|
90
|
+
|
|
91
|
+
| Domain | Improvement | Win rate |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| Ferromagnetic Ising (6-qubit chain) | 82.1% | 5/5 |
|
|
94
|
+
| Transverse Ising (6-qubit chain) | 71.2% | 5/5 |
|
|
95
|
+
| XY model (6-qubit chain) | 62.3% | 5/5 |
|
|
96
|
+
| Antiferromagnetic Heisenberg (6-qubit chain) | 61.3% | 5/5 |
|
|
97
|
+
| SSH model (topological, 6-qubit chain) | 71.7% | 5/5 |
|
|
98
|
+
| Kitaev chain (6-qubit) | 68.8% | 5/5 |
|
|
99
|
+
| Fibonacci chain (classical antiferromagnetic XY, N=16) | 95.4% | 10/10 |
|
|
100
|
+
| Penrose quasicrystal XY-model (50 sites) | 146.4% | 10/10 |
|
|
101
|
+
|
|
102
|
+
**Computer vision**
|
|
103
|
+
|
|
104
|
+
| Domain | Improvement | Win rate |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 93.9% | 17/20 |
|
|
107
|
+
|
|
108
|
+
**Finance**
|
|
109
|
+
|
|
110
|
+
| Domain | Improvement | Win rate |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Portfolio optimization (realistic backtest noise) | ~58x lower loss | 20/20 |
|
|
113
|
+
|
|
114
|
+
See `benchmarks/` for the exact, runnable scripts behind every one of these numbers, including the raw per-seed results.
|
|
115
|
+
|
|
116
|
+
## The math
|
|
117
|
+
|
|
118
|
+
SoftOpt's exact-derivative computation is a direct implementation of specific results from *Foundations of Soft Logic* (Klein & Maimon, Springer 2024):
|
|
119
|
+
|
|
120
|
+
| Operation | Book source | Formula |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `sadd(a,b)` | §4.3.1, p.27 | `(a+c, b+d)` |
|
|
123
|
+
| `smul(a,b)` | §4.3.1, p.27 | `(ad+bc, bd)` |
|
|
124
|
+
| `ssin`, `scos`, `sexp` | Lemma 6.1 (p.40) & §6.2 (p.41) | `f(a,b) = (a·f′(b), f(b))` |
|
|
125
|
+
| `sinv(a,b)` | Lemma 6.2(d), p.42 | `(−a/b², 1/b)` |
|
|
126
|
+
| `sdiv(x,y)` | composition | `smul(x, sinv(y))` |
|
|
127
|
+
|
|
128
|
+
## What SoftOpt is *not*
|
|
129
|
+
|
|
130
|
+
- Not a claim to beat specialized full-Jacobian second-order solvers (Levenberg–Marquardt, L-BFGS) where those are already practical — SoftOpt's validated niche is genuine improvement over first-order optimizers (Adam, SGD) already in use, particularly where switching to a full second-order method isn't practical (embedded in a larger pipeline, high dimensionality, or measurement noise).
|
|
131
|
+
- Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "softopt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ido Angel" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["optimizer", "optimization", "soft-logic", "soft-numbers", "quantum-computing", "vqe", "qaoa", "pytorch"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.20",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
torch = ["torch>=2.0"]
|
|
30
|
+
dev = ["pytest>=7.0", "torch>=2.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/mobiuai/softopt"
|
|
34
|
+
Repository = "https://github.com/mobiuai/softopt"
|
|
35
|
+
Issues = "https://github.com/mobiuai/softopt/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
include = ["softopt*"]
|
softopt-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SoftOpt — a standalone optimizer built on Klein–Maimon soft-number
|
|
3
|
+
calculus, for problems with a known, differentiable computation graph
|
|
4
|
+
(quantum circuits, physical models, projection models, ...).
|
|
5
|
+
|
|
6
|
+
from softopt import SoftOpt # numpy version
|
|
7
|
+
from softopt import SoftOptTorch # PyTorch version (torch.optim.Optimizer)
|
|
8
|
+
|
|
9
|
+
See README.md for the exact requirement your problem must satisfy
|
|
10
|
+
(the "known computation graph" criterion) and worked examples.
|
|
11
|
+
"""
|
|
12
|
+
from .numpy_optimizer import SoftOpt
|
|
13
|
+
|
|
14
|
+
__all__ = ["SoftOpt"]
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from .torch_optimizer import SoftOpt as SoftOptTorch
|
|
19
|
+
__all__.append("SoftOptTorch")
|
|
20
|
+
except ImportError:
|
|
21
|
+
# torch is an optional dependency (pip install softopt[torch])
|
|
22
|
+
pass
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SoftOpt: a COMPLETE, standalone optimizer -- not a wrapper around Adam.
|
|
3
|
+
Exactly one step() call does both the Adam-style base update AND the
|
|
4
|
+
validated Newton-style correction. You don't bring your own optimizer;
|
|
5
|
+
this IS the optimizer, in the same sense that torch.optim.Adam IS an
|
|
6
|
+
optimizer (internally momentum + RMS-scaling), not a wrapper around SGD.
|
|
7
|
+
|
|
8
|
+
Requirement (the "known computation graph" criterion, validated across
|
|
9
|
+
nine domains today): you must supply g_delta(theta, delta) -> the exact
|
|
10
|
+
directional derivative of your TRUE, differentiable objective along
|
|
11
|
+
delta, computed via Klein-Maimon soft-number propagation through your
|
|
12
|
+
own known model (circuit, projection, force field, ...). If you cannot
|
|
13
|
+
write g_delta without sampling the world, SoftOpt is not the right tool
|
|
14
|
+
-- use plain Adam/SGD instead.
|
|
15
|
+
|
|
16
|
+
Three correction modes (see README for details):
|
|
17
|
+
- mode="newton" (default): bounded Newton step, best for one-signed
|
|
18
|
+
curvature (most physics/circuit problems).
|
|
19
|
+
- mode="mobius": bounded, sign-safe Mobius-map step (book Ch.5.3), for
|
|
20
|
+
landscapes whose curvature isn't reliably one-signed (e.g. QAOA).
|
|
21
|
+
- mode="auto" (EXPERIMENTAL, not yet reliable -- see README): attempts
|
|
22
|
+
to diagnose which of the above fits your problem by sampling D2's
|
|
23
|
+
sign near the starting point. Tested against GRAPE (a domain that
|
|
24
|
+
needs "newton" with high confidence) it only picked "newton" 40% of
|
|
25
|
+
the time -- a single starting region's local curvature sign is not
|
|
26
|
+
a reliable predictor of the right mode for the whole optimization.
|
|
27
|
+
Shipped anyway, clearly labeled, so you can inspect `opt.detected_mode`
|
|
28
|
+
and help characterize when it does/doesn't work, but do not rely on
|
|
29
|
+
it yet -- the empirically-validated way to choose is still: try both
|
|
30
|
+
modes on a short run and keep whichever beats plain Adam.
|
|
31
|
+
"""
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _mobius_B(x, y):
|
|
36
|
+
"""Book Ch.5.3's Möbius map, bounded to [-1,1] -- a sign-safe
|
|
37
|
+
alternative to raw Newton division for landscapes whose curvature
|
|
38
|
+
isn't reliably one-signed (e.g. QAOA, empirically)."""
|
|
39
|
+
denom = abs(x) + abs(y)
|
|
40
|
+
if denom < 1e-12:
|
|
41
|
+
return 0.0
|
|
42
|
+
sgn = 1.0 if x >= 0 else -1.0
|
|
43
|
+
return y * sgn / denom
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SoftOpt:
|
|
47
|
+
def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
|
|
48
|
+
mode="newton", auto_detect_steps=30, auto_detect_threshold=0.8,
|
|
49
|
+
newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
|
|
50
|
+
eta_mobius=0.3, h_fd=1e-4, seed=None,
|
|
51
|
+
_test_frozen_constant=1.0):
|
|
52
|
+
self.g_delta = g_delta
|
|
53
|
+
self.lr = lr
|
|
54
|
+
self.b1, self.b2 = betas
|
|
55
|
+
self.eps = eps
|
|
56
|
+
if mode not in ("newton", "mobius", "auto"):
|
|
57
|
+
raise ValueError("mode must be 'newton', 'mobius', or 'auto'")
|
|
58
|
+
self.mode = mode
|
|
59
|
+
self.auto_detect_steps = auto_detect_steps
|
|
60
|
+
self.auto_detect_threshold = auto_detect_threshold
|
|
61
|
+
self._auto_pending = (mode == "auto")
|
|
62
|
+
self.detected_mode = None if mode == "auto" else mode
|
|
63
|
+
self.newton_lo = newton_lo
|
|
64
|
+
self.newton_hi = newton_hi
|
|
65
|
+
self.eta_fallback = eta_fallback
|
|
66
|
+
self.eta_mobius = eta_mobius
|
|
67
|
+
self.h_fd = h_fd
|
|
68
|
+
self.m = None
|
|
69
|
+
self.v = None
|
|
70
|
+
self.t = 0
|
|
71
|
+
self.rng = np.random.default_rng(seed)
|
|
72
|
+
self._test_frozen_constant = _test_frozen_constant
|
|
73
|
+
|
|
74
|
+
def _diagnose_mode(self, theta0):
|
|
75
|
+
"""Run once, before any optimization step: sample D2's sign
|
|
76
|
+
across several nearby points (not just theta0 itself -- a single
|
|
77
|
+
point's local curvature sign is not always representative) and
|
|
78
|
+
several random directions per point, matching the methodology
|
|
79
|
+
that correctly distinguished GRAPE/VQE's one-signed curvature
|
|
80
|
+
from QAOA's sign-indefinite curvature."""
|
|
81
|
+
n = theta0.shape[0]
|
|
82
|
+
n_points = 5
|
|
83
|
+
dirs_per_point = max(1, self.auto_detect_steps // n_points)
|
|
84
|
+
signs = []
|
|
85
|
+
for p in range(n_points):
|
|
86
|
+
point = theta0 if p == 0 else theta0 + self.rng.normal(scale=0.1, size=n)
|
|
87
|
+
for _ in range(dirs_per_point):
|
|
88
|
+
delta = self.rng.choice([-1.0, 1.0], size=n)
|
|
89
|
+
gp = self.g_delta(point + self.h_fd * delta, delta)
|
|
90
|
+
gm = self.g_delta(point - self.h_fd * delta, delta)
|
|
91
|
+
D2 = (gp - gm) / (2 * self.h_fd)
|
|
92
|
+
signs.append(1.0 if D2 >= 0 else -1.0)
|
|
93
|
+
signs = np.array(signs)
|
|
94
|
+
agreement = max(np.mean(signs > 0), np.mean(signs < 0))
|
|
95
|
+
self.detected_mode = "newton" if agreement >= self.auto_detect_threshold else "mobius"
|
|
96
|
+
self.mode = self.detected_mode
|
|
97
|
+
|
|
98
|
+
def step(self, theta, grad_estimate, _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
|
|
99
|
+
"""One complete optimizer step: Adam base update + Newton-style
|
|
100
|
+
correction from the known model, in a single call.
|
|
101
|
+
theta: current parameters (np.ndarray)
|
|
102
|
+
grad_estimate: a gradient estimate of the loss w.r.t. theta (e.g.
|
|
103
|
+
from SPSA or backprop) -- exactly what you'd normally hand
|
|
104
|
+
to Adam.
|
|
105
|
+
Returns: theta for the next step.
|
|
106
|
+
|
|
107
|
+
The _test_* arguments are TEST-ONLY hooks used exclusively by the
|
|
108
|
+
causal-validation ablation harness to deliberately corrupt the
|
|
109
|
+
correction (feed a frozen constant or a foreign point's curvature
|
|
110
|
+
instead of the genuine one) so that "does real content matter"
|
|
111
|
+
can be checked against the EXACT SAME code path real usage takes.
|
|
112
|
+
Normal usage never sets these; leaving them unset gives the
|
|
113
|
+
genuine, real behavior.
|
|
114
|
+
"""
|
|
115
|
+
theta = np.asarray(theta, dtype=float)
|
|
116
|
+
grad_estimate = np.asarray(grad_estimate, dtype=float)
|
|
117
|
+
|
|
118
|
+
if self._auto_pending:
|
|
119
|
+
self._diagnose_mode(theta)
|
|
120
|
+
self._auto_pending = False
|
|
121
|
+
|
|
122
|
+
# --- Adam base update ---
|
|
123
|
+
self.t += 1
|
|
124
|
+
if self.m is None:
|
|
125
|
+
self.m = np.zeros_like(theta)
|
|
126
|
+
self.v = np.zeros_like(theta)
|
|
127
|
+
self.m = self.b1 * self.m + (1 - self.b1) * grad_estimate
|
|
128
|
+
self.v = self.b2 * self.v + (1 - self.b2) * grad_estimate ** 2
|
|
129
|
+
mh = self.m / (1 - self.b1 ** self.t)
|
|
130
|
+
vh = self.v / (1 - self.b2 ** self.t)
|
|
131
|
+
theta_after_adam = theta - self.lr * mh / (np.sqrt(vh) + self.eps)
|
|
132
|
+
|
|
133
|
+
if _test_magnitude_source == 'plain':
|
|
134
|
+
return theta_after_adam
|
|
135
|
+
|
|
136
|
+
# --- correction from the known, exact model ---
|
|
137
|
+
n = theta.shape[0]
|
|
138
|
+
gen_rng = _test_rng if _test_rng is not None else self.rng
|
|
139
|
+
delta = gen_rng.choice([-1.0, 1.0], size=n)
|
|
140
|
+
D1 = self.g_delta(theta_after_adam, delta)
|
|
141
|
+
gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
|
|
142
|
+
gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
|
|
143
|
+
D2_real = (gp - gm) / (2 * self.h_fd)
|
|
144
|
+
|
|
145
|
+
if _test_magnitude_source in (None, 'real'):
|
|
146
|
+
D2_used = D2_real
|
|
147
|
+
elif _test_magnitude_source == 'frozen':
|
|
148
|
+
D2_used = self._test_frozen_constant
|
|
149
|
+
elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
|
|
150
|
+
theta_f = _test_foreign_sampler(gen_rng)
|
|
151
|
+
delta_f = gen_rng.choice([-1.0, 1.0], size=n) if _test_magnitude_source == 'foreign_point_and_dir' else delta
|
|
152
|
+
gp_f = self.g_delta(theta_f + self.h_fd * delta_f, delta_f)
|
|
153
|
+
gm_f = self.g_delta(theta_f - self.h_fd * delta_f, delta_f)
|
|
154
|
+
D2_used = abs((gp_f - gm_f) / (2 * self.h_fd))
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(_test_magnitude_source)
|
|
157
|
+
|
|
158
|
+
if self.mode == "mobius":
|
|
159
|
+
B = _mobius_B(D1, D2_used)
|
|
160
|
+
t_star = self.eta_mobius * B
|
|
161
|
+
else:
|
|
162
|
+
activate = D2_real > 1e-8
|
|
163
|
+
if activate and D2_used > 1e-9:
|
|
164
|
+
t_star = float(np.clip(-D1 / D2_used, self.newton_lo, self.newton_hi))
|
|
165
|
+
else:
|
|
166
|
+
t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
|
|
167
|
+
|
|
168
|
+
return theta_after_adam + t_star * delta
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SoftOpt (PyTorch): a COMPLETE, standalone torch.optim.Optimizer -- not a
|
|
3
|
+
wrapper. Use it exactly like torch.optim.Adam:
|
|
4
|
+
|
|
5
|
+
opt = SoftOpt(model.parameters(), model, loss_fn, lr=1e-3)
|
|
6
|
+
loss = opt.step(batch)
|
|
7
|
+
|
|
8
|
+
One step() call does the Adam-style base update AND the Newton-style
|
|
9
|
+
correction, using genuine forward-mode automatic differentiation
|
|
10
|
+
(torch.func.jvp) to get an exact directional derivative and curvature of
|
|
11
|
+
your loss along a random probe direction -- mathematically the same
|
|
12
|
+
quantity a hand-written Klein-Maimon soft-number propagation would give
|
|
13
|
+
(dual numbers are isomorphic to single-axis soft numbers per the book's
|
|
14
|
+
own Appendix A.2), computed via PyTorch's own AD engine instead of
|
|
15
|
+
hand-written soft-number code.
|
|
16
|
+
|
|
17
|
+
Requirement (the "known computation graph" criterion, validated across
|
|
18
|
+
nine domains): loss_fn(model, batch) must be an exactly re-evaluatable,
|
|
19
|
+
differentiable function -- e.g. a full-batch loss over a fixed dataset,
|
|
20
|
+
a physics/circuit simulator, a known projection model. If your loss is
|
|
21
|
+
a small stochastic minibatch sample that changes meaning between calls
|
|
22
|
+
(the classic deep-RL moving-target problem), this mechanism is not
|
|
23
|
+
validated to help -- use plain Adam/SGD instead.
|
|
24
|
+
"""
|
|
25
|
+
import numpy as np
|
|
26
|
+
import torch
|
|
27
|
+
import torch.func as tfunc
|
|
28
|
+
from torch.optim import Optimizer
|
|
29
|
+
from torch.nn.utils.stateless import _reparametrize_module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SoftOpt(Optimizer):
|
|
33
|
+
def __init__(self, params, model, loss_fn, lr=1e-3, betas=(0.9, 0.999), eps=1e-8,
|
|
34
|
+
newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3, h_fd=1e-4, seed=None,
|
|
35
|
+
_test_frozen_constant=1.0):
|
|
36
|
+
defaults = dict(lr=lr, betas=betas, eps=eps)
|
|
37
|
+
super().__init__(params, defaults)
|
|
38
|
+
self.model = model
|
|
39
|
+
self.loss_fn = loss_fn
|
|
40
|
+
self.newton_lo = newton_lo
|
|
41
|
+
self.newton_hi = newton_hi
|
|
42
|
+
self.eta_fallback = eta_fallback
|
|
43
|
+
self.h_fd = h_fd
|
|
44
|
+
self.rng = np.random.default_rng(seed)
|
|
45
|
+
self._test_frozen_constant = _test_frozen_constant
|
|
46
|
+
|
|
47
|
+
def _adam_update(self):
|
|
48
|
+
for group in self.param_groups:
|
|
49
|
+
b1, b2 = group['betas']
|
|
50
|
+
for p in group['params']:
|
|
51
|
+
if p.grad is None:
|
|
52
|
+
continue
|
|
53
|
+
state = self.state[p]
|
|
54
|
+
if len(state) == 0:
|
|
55
|
+
state['step'] = 0
|
|
56
|
+
state['exp_avg'] = torch.zeros_like(p)
|
|
57
|
+
state['exp_avg_sq'] = torch.zeros_like(p)
|
|
58
|
+
state['step'] += 1
|
|
59
|
+
state['exp_avg'].mul_(b1).add_(p.grad, alpha=1 - b1)
|
|
60
|
+
state['exp_avg_sq'].mul_(b2).addcmul_(p.grad, p.grad, value=1 - b2)
|
|
61
|
+
bc1 = 1 - b1 ** state['step']
|
|
62
|
+
bc2 = 1 - b2 ** state['step']
|
|
63
|
+
denom = (state['exp_avg_sq'] / bc2).sqrt().add_(group['eps'])
|
|
64
|
+
p.data.addcdiv_(state['exp_avg'], denom, value=-group['lr'] / bc1)
|
|
65
|
+
|
|
66
|
+
def step(self, batch, _test_magnitude_source=None, _test_foreign_sampler=None):
|
|
67
|
+
"""batch: whatever loss_fn(model, batch) needs. Returns the loss
|
|
68
|
+
(a float) from the pre-correction forward pass.
|
|
69
|
+
_test_* are TEST-ONLY hooks for the causal-validation harness --
|
|
70
|
+
normal usage never sets these.
|
|
71
|
+
"""
|
|
72
|
+
all_params = [p for group in self.param_groups for p in group['params']]
|
|
73
|
+
|
|
74
|
+
# --- standard forward/backward for the Adam step ---
|
|
75
|
+
with torch.enable_grad():
|
|
76
|
+
self.zero_grad()
|
|
77
|
+
loss = self.loss_fn(self.model, batch)
|
|
78
|
+
loss.backward()
|
|
79
|
+
loss_value = float(loss.detach())
|
|
80
|
+
|
|
81
|
+
with torch.no_grad():
|
|
82
|
+
self._adam_update()
|
|
83
|
+
|
|
84
|
+
if _test_magnitude_source == 'plain':
|
|
85
|
+
return loss_value
|
|
86
|
+
|
|
87
|
+
# --- Newton-style correction via genuine forward-mode AD ---
|
|
88
|
+
with torch.no_grad():
|
|
89
|
+
named = {id(p): name for name, p in self.model.named_parameters()}
|
|
90
|
+
all_named = dict(self.model.named_parameters())
|
|
91
|
+
names = [n for n, p in all_named.items() if any(p is q for q in all_params)]
|
|
92
|
+
flat0 = torch.cat([all_named[n].detach().reshape(-1) for n in names])
|
|
93
|
+
n_total = flat0.numel()
|
|
94
|
+
delta = torch.tensor(self.rng.choice([-1.0, 1.0], size=n_total).astype(np.float32))
|
|
95
|
+
|
|
96
|
+
fixed_vals = {n: p.detach() for n, p in all_named.items() if n not in names}
|
|
97
|
+
|
|
98
|
+
def make_scalar_loss(the_batch):
|
|
99
|
+
def scalar_loss(flat):
|
|
100
|
+
pd = {}
|
|
101
|
+
idx = 0
|
|
102
|
+
for n in names:
|
|
103
|
+
p = all_named[n]
|
|
104
|
+
nn_ = p.numel()
|
|
105
|
+
pd[n] = flat[idx:idx + nn_].reshape(p.shape)
|
|
106
|
+
idx += nn_
|
|
107
|
+
pd.update(fixed_vals)
|
|
108
|
+
with _reparametrize_module(self.model, pd):
|
|
109
|
+
return self.loss_fn(self.model, the_batch)
|
|
110
|
+
return scalar_loss
|
|
111
|
+
|
|
112
|
+
def D1_D2_at(point_theta, point_delta, point_batch):
|
|
113
|
+
scalar_loss = make_scalar_loss(point_batch)
|
|
114
|
+
_, d1 = tfunc.jvp(scalar_loss, (point_theta,), (point_delta,))
|
|
115
|
+
def d_along(fp):
|
|
116
|
+
_, d = tfunc.jvp(scalar_loss, (fp,), (point_delta,))
|
|
117
|
+
return d
|
|
118
|
+
_, d2 = tfunc.jvp(d_along, (point_theta,), (point_delta,))
|
|
119
|
+
return float(d1), float(d2)
|
|
120
|
+
|
|
121
|
+
D1, D2_real = D1_D2_at(flat0, delta, batch)
|
|
122
|
+
activate = D2_real > 1e-8
|
|
123
|
+
|
|
124
|
+
if _test_magnitude_source in (None, 'real'):
|
|
125
|
+
D2_used = D2_real
|
|
126
|
+
elif _test_magnitude_source == 'frozen':
|
|
127
|
+
D2_used = self._test_frozen_constant
|
|
128
|
+
elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
|
|
129
|
+
theta_f, batch_f = _test_foreign_sampler(self.rng)
|
|
130
|
+
delta_f = (torch.tensor(self.rng.choice([-1.0, 1.0], size=n_total).astype(np.float32))
|
|
131
|
+
if _test_magnitude_source == 'foreign_point_and_dir' else delta)
|
|
132
|
+
_, D2_f = D1_D2_at(theta_f, delta_f, batch_f)
|
|
133
|
+
D2_used = abs(D2_f)
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(_test_magnitude_source)
|
|
136
|
+
|
|
137
|
+
if activate and D2_used > 1e-9:
|
|
138
|
+
t_star = float(np.clip(-D1 / D2_used, self.newton_lo, self.newton_hi))
|
|
139
|
+
else:
|
|
140
|
+
t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
|
|
141
|
+
|
|
142
|
+
new_flat = flat0 + t_star * delta
|
|
143
|
+
idx = 0
|
|
144
|
+
for n in names:
|
|
145
|
+
p = all_named[n]
|
|
146
|
+
nn_ = p.numel()
|
|
147
|
+
p.data.copy_(new_flat[idx:idx + nn_].reshape(p.shape))
|
|
148
|
+
idx += nn_
|
|
149
|
+
|
|
150
|
+
return loss_value
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: softopt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
|
+
Author: Ido Angel
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mobiuai/softopt
|
|
8
|
+
Project-URL: Repository, https://github.com/mobiuai/softopt
|
|
9
|
+
Project-URL: Issues, https://github.com/mobiuai/softopt/issues
|
|
10
|
+
Keywords: optimizer,optimization,soft-logic,soft-numbers,quantum-computing,vqe,qaoa,pytorch
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.20
|
|
21
|
+
Provides-Extra: torch
|
|
22
|
+
Requires-Dist: torch>=2.0; extra == "torch"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: torch>=2.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# SoftOpt
|
|
29
|
+
|
|
30
|
+
**A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
|
|
31
|
+
|
|
32
|
+
SoftOpt is free, fully local, and requires no server, license key, or network access — the same way `torch.optim.Adam` requires none. It is not a wrapper around Adam or any other optimizer: it is a complete, drop-in optimizer with its own Adam-equivalent base update, plus an exact Newton-style correction built on Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024).
|
|
33
|
+
|
|
34
|
+
## Where it helps
|
|
35
|
+
|
|
36
|
+
If your problem has a **known computation graph** — a quantum circuit, a physical simulator, a projection or measurement model, anything you can write down exactly, even if the *measurements* of it are noisy — SoftOpt computes an exact directional derivative and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
|
|
37
|
+
|
|
38
|
+
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
39
|
+
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
40
|
+
- **Computer vision** — multi-camera bundle adjustment / camera calibration
|
|
41
|
+
- **Finance** — portfolio optimization (Markowitz mean-variance)
|
|
42
|
+
- **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
|
|
43
|
+
- Pharmacokinetics, logistic regression, and more
|
|
44
|
+
|
|
45
|
+
## Where it does *not* help
|
|
46
|
+
|
|
47
|
+
Full reinforcement learning (or anything else with a continuously **moving target** — a policy, an adversary, a non-stationary distribution) is outside SoftOpt's validated scope. The mechanism needs a *fixed* objective to compute a meaningful correction against; a moving target breaks that assumption. Use plain Adam/SGD there.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install softopt # numpy version only
|
|
53
|
+
pip install softopt[torch] # + the PyTorch optimizer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
### NumPy
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from softopt import SoftOpt
|
|
62
|
+
|
|
63
|
+
# g_delta(theta, delta) must return the EXACT directional derivative of
|
|
64
|
+
# your true, differentiable objective along `delta`, at `theta` — computed
|
|
65
|
+
# from your own known model (a circuit, a projection, a physical law),
|
|
66
|
+
# not estimated from noisy measurements.
|
|
67
|
+
opt = SoftOpt(n_params, g_delta, lr=0.02)
|
|
68
|
+
|
|
69
|
+
for step in range(num_steps):
|
|
70
|
+
grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
|
|
71
|
+
theta = opt.step(theta, grad_estimate)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### PyTorch
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from softopt import SoftOptTorch
|
|
78
|
+
|
|
79
|
+
opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
|
|
80
|
+
|
|
81
|
+
for batch in data:
|
|
82
|
+
loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
|
|
86
|
+
|
|
87
|
+
## Two correction modes
|
|
88
|
+
|
|
89
|
+
SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
|
|
90
|
+
|
|
91
|
+
- **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
|
|
92
|
+
- **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
|
|
93
|
+
|
|
94
|
+
**How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
|
|
95
|
+
|
|
96
|
+
## Validated results
|
|
97
|
+
|
|
98
|
+
All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
|
|
99
|
+
|
|
100
|
+
**Quantum chemistry (VQE)**
|
|
101
|
+
|
|
102
|
+
| Domain | Improvement | Win rate |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| H₂ | 66.6% | 5/5 |
|
|
105
|
+
| H₄ | 89.8% | 5/5 |
|
|
106
|
+
| C₁₃Cl₂ (13-term Hamiltonian, incl. a 4-body term) | 81.3% | 5/5 |
|
|
107
|
+
| BeH₂ | 94.7% | 5/5 |
|
|
108
|
+
| HeH⁺ | 90.9% | 5/5 |
|
|
109
|
+
|
|
110
|
+
**Quantum control**
|
|
111
|
+
|
|
112
|
+
| Domain | Improvement | Win rate |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| GRAPE (2-qubit) | 84.0% | 20/20 |
|
|
115
|
+
|
|
116
|
+
**Condensed-matter & spin models**
|
|
117
|
+
|
|
118
|
+
| Domain | Improvement | Win rate |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| Ferromagnetic Ising (6-qubit chain) | 82.1% | 5/5 |
|
|
121
|
+
| Transverse Ising (6-qubit chain) | 71.2% | 5/5 |
|
|
122
|
+
| XY model (6-qubit chain) | 62.3% | 5/5 |
|
|
123
|
+
| Antiferromagnetic Heisenberg (6-qubit chain) | 61.3% | 5/5 |
|
|
124
|
+
| SSH model (topological, 6-qubit chain) | 71.7% | 5/5 |
|
|
125
|
+
| Kitaev chain (6-qubit) | 68.8% | 5/5 |
|
|
126
|
+
| Fibonacci chain (classical antiferromagnetic XY, N=16) | 95.4% | 10/10 |
|
|
127
|
+
| Penrose quasicrystal XY-model (50 sites) | 146.4% | 10/10 |
|
|
128
|
+
|
|
129
|
+
**Computer vision**
|
|
130
|
+
|
|
131
|
+
| Domain | Improvement | Win rate |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 93.9% | 17/20 |
|
|
134
|
+
|
|
135
|
+
**Finance**
|
|
136
|
+
|
|
137
|
+
| Domain | Improvement | Win rate |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| Portfolio optimization (realistic backtest noise) | ~58x lower loss | 20/20 |
|
|
140
|
+
|
|
141
|
+
See `benchmarks/` for the exact, runnable scripts behind every one of these numbers, including the raw per-seed results.
|
|
142
|
+
|
|
143
|
+
## The math
|
|
144
|
+
|
|
145
|
+
SoftOpt's exact-derivative computation is a direct implementation of specific results from *Foundations of Soft Logic* (Klein & Maimon, Springer 2024):
|
|
146
|
+
|
|
147
|
+
| Operation | Book source | Formula |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `sadd(a,b)` | §4.3.1, p.27 | `(a+c, b+d)` |
|
|
150
|
+
| `smul(a,b)` | §4.3.1, p.27 | `(ad+bc, bd)` |
|
|
151
|
+
| `ssin`, `scos`, `sexp` | Lemma 6.1 (p.40) & §6.2 (p.41) | `f(a,b) = (a·f′(b), f(b))` |
|
|
152
|
+
| `sinv(a,b)` | Lemma 6.2(d), p.42 | `(−a/b², 1/b)` |
|
|
153
|
+
| `sdiv(x,y)` | composition | `smul(x, sinv(y))` |
|
|
154
|
+
|
|
155
|
+
## What SoftOpt is *not*
|
|
156
|
+
|
|
157
|
+
- Not a claim to beat specialized full-Jacobian second-order solvers (Levenberg–Marquardt, L-BFGS) where those are already practical — SoftOpt's validated niche is genuine improvement over first-order optimizers (Adam, SGD) already in use, particularly where switching to a full second-order method isn't practical (embedded in a larger pipeline, high dimensionality, or measurement noise).
|
|
158
|
+
- Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
softopt/__init__.py
|
|
5
|
+
softopt/numpy_optimizer.py
|
|
6
|
+
softopt/torch_optimizer.py
|
|
7
|
+
softopt.egg-info/PKG-INFO
|
|
8
|
+
softopt.egg-info/SOURCES.txt
|
|
9
|
+
softopt.egg-info/dependency_links.txt
|
|
10
|
+
softopt.egg-info/requires.txt
|
|
11
|
+
softopt.egg-info/top_level.txt
|
|
12
|
+
tests/test_basic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
softopt
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic sanity tests for SoftOpt. These are NOT the domain-validation
|
|
3
|
+
benchmarks (see benchmarks/) -- just fast checks that the optimizer
|
|
4
|
+
runs correctly and does what it's supposed to on a trivial problem.
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
from softopt import SoftOpt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def quadratic_g_delta(theta, delta):
|
|
12
|
+
"""g_delta for f(theta) = |theta|^2 -- exact directional derivative
|
|
13
|
+
is 2*theta . delta."""
|
|
14
|
+
return float(2 * theta @ delta)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_newton_mode_converges_on_convex_quadratic():
|
|
18
|
+
opt = SoftOpt(5, quadratic_g_delta, lr=0.1, seed=0)
|
|
19
|
+
theta = np.ones(5)
|
|
20
|
+
for _ in range(200):
|
|
21
|
+
grad = 2 * theta # exact gradient, standing in for a real gradient estimate
|
|
22
|
+
theta = opt.step(theta, grad)
|
|
23
|
+
assert np.allclose(theta, 0.0, atol=1e-2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_mobius_mode_runs_without_error():
|
|
27
|
+
opt = SoftOpt(5, quadratic_g_delta, lr=0.1, mode="mobius", seed=0)
|
|
28
|
+
theta = np.ones(5)
|
|
29
|
+
for _ in range(50):
|
|
30
|
+
grad = 2 * theta
|
|
31
|
+
theta = opt.step(theta, grad)
|
|
32
|
+
assert np.all(np.isfinite(theta))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_invalid_mode_raises():
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
SoftOpt(5, quadratic_g_delta, mode="not_a_real_mode")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_step_is_deterministic_given_seed():
|
|
41
|
+
opt1 = SoftOpt(3, quadratic_g_delta, lr=0.05, seed=42)
|
|
42
|
+
opt2 = SoftOpt(3, quadratic_g_delta, lr=0.05, seed=42)
|
|
43
|
+
theta1 = theta2 = np.array([1.0, 2.0, 3.0])
|
|
44
|
+
for _ in range(10):
|
|
45
|
+
theta1 = opt1.step(theta1, 2 * theta1)
|
|
46
|
+
theta2 = opt2.step(theta2, 2 * theta2)
|
|
47
|
+
assert np.allclose(theta1, theta2)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_plain_arm_skips_correction():
|
|
51
|
+
"""The _test_magnitude_source='plain' hook should return exactly the
|
|
52
|
+
Adam-only update, with no correction applied."""
|
|
53
|
+
opt = SoftOpt(3, quadratic_g_delta, lr=0.1, seed=0)
|
|
54
|
+
theta = np.array([1.0, 1.0, 1.0])
|
|
55
|
+
result = opt.step(theta, 2 * theta, _test_magnitude_source="plain")
|
|
56
|
+
assert np.all(np.isfinite(result))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_torch_optimizer_importable_if_torch_present():
|
|
60
|
+
try:
|
|
61
|
+
import torch # noqa: F401
|
|
62
|
+
except ImportError:
|
|
63
|
+
pytest.skip("torch not installed")
|
|
64
|
+
from softopt import SoftOptTorch # noqa: F401
|