softopt 0.6.4__tar.gz → 0.7.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.
Files changed (24) hide show
  1. {softopt-0.6.4/softopt.egg-info → softopt-0.7.0}/PKG-INFO +115 -51
  2. {softopt-0.6.4 → softopt-0.7.0}/README.md +112 -48
  3. {softopt-0.6.4 → softopt-0.7.0}/pyproject.toml +3 -3
  4. {softopt-0.6.4 → softopt-0.7.0}/softopt/__init__.py +7 -7
  5. softopt-0.7.0/softopt/calibrate.py +159 -0
  6. {softopt-0.6.4 → softopt-0.7.0}/softopt/compile.py +12 -1
  7. {softopt-0.6.4 → softopt-0.7.0}/softopt/numpy_optimizer.py +40 -1
  8. {softopt-0.6.4 → softopt-0.7.0/softopt.egg-info}/PKG-INFO +115 -51
  9. {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/SOURCES.txt +2 -0
  10. softopt-0.7.0/tests/test_calibrate.py +86 -0
  11. {softopt-0.6.4 → softopt-0.7.0}/LICENSE +0 -0
  12. {softopt-0.6.4 → softopt-0.7.0}/setup.cfg +0 -0
  13. {softopt-0.6.4 → softopt-0.7.0}/softopt/soft_number.py +0 -0
  14. {softopt-0.6.4 → softopt-0.7.0}/softopt/torch_optimizer.py +0 -0
  15. {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/dependency_links.txt +0 -0
  16. {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/requires.txt +0 -0
  17. {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/top_level.txt +0 -0
  18. {softopt-0.6.4 → softopt-0.7.0}/tests/test_basic.py +0 -0
  19. {softopt-0.6.4 → softopt-0.7.0}/tests/test_causal.py +0 -0
  20. {softopt-0.6.4 → softopt-0.7.0}/tests/test_compile.py +0 -0
  21. {softopt-0.6.4 → softopt-0.7.0}/tests/test_exact_d2.py +0 -0
  22. {softopt-0.6.4 → softopt-0.7.0}/tests/test_measured_slope.py +0 -0
  23. {softopt-0.6.4 → softopt-0.7.0}/tests/test_soft_number.py +0 -0
  24. {softopt-0.6.4 → softopt-0.7.0}/tests/test_torch.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.6.4
4
- Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
3
+ Version: 0.7.0
4
+ Summary: An optimization layer that closes the gap between your model and the real system, built on Klein–Maimon soft-number calculus
5
5
  Author: Ido Angel
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/mobiuai/softopt
8
8
  Project-URL: Repository, https://github.com/mobiuai/softopt
9
9
  Project-URL: Issues, https://github.com/mobiuai/softopt/issues
10
- Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,quantum-computing,vqe
10
+ Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,calibration,digital-twin,sim-to-real,quantum-computing,vqe
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -26,66 +26,126 @@ Dynamic: license-file
26
26
 
27
27
  # SoftOpt
28
28
 
29
- **A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
29
+ **An optimization layer that closes the gap between your model and the real system.**
30
30
 
31
- Each step is an update followed by an exact Newton correction computed from your own model, through Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024). Free, open source, and runs locally.
31
+ You have a model of your system — a simulator, a circuit, a physical law, a pharmacokinetic model — and you
32
+ measure the real thing, where every measurement costs time or money. SoftOpt sits on top of Adam and, on every
33
+ step, takes an exact Newton correction from your model through Klein–Maimon soft-number calculus (*Foundations
34
+ of Soft Logic*, Klein & Maimon, Springer 2024). With `calibrate=True` the same measurements also correct the
35
+ model's uncertain parameters while the optimization runs, so the correction keeps pointing at the real system's
36
+ optimum instead of the model's.
32
37
 
33
- ## Is SoftOpt the right tool for your problem?
38
+ **When your model is exact** — a quantum circuit, a known physical law — SoftOpt gets closer to the true optimum
39
+ than Adam and COBYLA. On H₂ on IBM's FakeFez noise model it reaches chemical accuracy in 10 of 10 runs against
40
+ COBYLA's 3 of 10 (0.11 vs 5.81 mHa, same 180 readings); on the H₄ molecule (8 qubits, 185 Pauli terms) its gap to
41
+ the ground state is 67% smaller than Adam's, 10/10; in two-qubit quantum control, 84% smaller, 20/20.
34
42
 
35
- One question decides it, and you can answer it without running anything.
43
+ **When your model is approximately right** — the usual case on real systems — `calibrate=True` closes the gap.
44
+ Across eleven domains — quantum chemistry, beam steering, robotics, chemical engineering, drug dosing, finance,
45
+ building control, batteries, epidemiology, machine learning and an exact-model control — SoftOpt with
46
+ `calibrate=True` reaches the quality Adam reaches after 200 measurements with **2.65× fewer measurements**
47
+ (median; 1.35× to 5.4× by domain).
36
48
 
37
- **Can you get a residual vector?** That is: can you compute
38
- `model(theta) - data` and get back an *array*, rather than a single number?
49
+ Free, open source, and runs locally.
39
50
 
40
- - **Yes** → use `scipy.optimize.least_squares`. It builds the full Jacobian and
41
- exploits the least-squares structure, and it will beat SoftOpt on both
42
- accuracy and speed. This holds regardless of how large or how noisy the
43
- problem is. We measured it: on solar-cell parameter extraction, `least_squares`
44
- reached 7.9 mA against SoftOpt's 37.3 mA, and did it 21x faster.
51
+ ## When to use it
45
52
 
46
- - **No** → SoftOpt is built for this case. Each evaluation returns **one number,
47
- and it is noisy**: a quantum measurement, a laser-tracker sweep, a backtest, a
48
- simulator that returns a score. There is no residual vector for
49
- `least_squares` to work with, and finite-difference methods like L-BFGS-B end
50
- up differentiating the noise rather than the model.
53
+ **You have a model of your system and you measure the real one?** Use `calibrate=True` and name the model
54
+ parameters you are not sure of, with a generous estimate of how far off they may be.
51
55
 
52
- SoftOpt's advantage comes from taking the derivative **from your model** instead
53
- of from your measurements. That only matters when the measurements are the noisy
54
- part. Every validated domain below is of the second kind; that is not a
55
- convenience of selection, it is where the mechanism applies.
56
+ ```python
57
+ from softopt import SoftOpt, soft_compile, spsa_gradient
58
+
59
+ def model(theta, beta): # your simulator; beta = offsets of its uncertain parameters (0 = nominal)
60
+ ...
61
+
62
+ g = soft_compile(model, n_params=n, n_bias=k)
63
+ opt = SoftOpt(n, g, lr=0.05, calibrate=True, bias_scale=0.2) # bias_scale: expected size of beta (generous)
64
+
65
+ for step in range(num_steps):
66
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 measurements of the real system
67
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
68
+
69
+ opt.calibration # the current estimates: beta, contrast, and the discrepancy the model cannot explain
70
+ ```
56
71
 
57
- ## Where it helps
72
+ Everything else stays as it was: the same Adam update, the same measurements, no extra readings. When unsure of
73
+ `bias_scale`, err on the wide side.
58
74
 
59
- 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 of that model on every step, plus the curvature along the same direction, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
75
+ | your situation | the tool |
76
+ |---|---|
77
+ | a model with uncertain parameters + measurements of the real system | **SoftOpt, `calibrate=True`** |
78
+ | an exact model + noisy measurements | SoftOpt, default settings |
79
+ | you can compute a residual vector `model(theta) - data` | `scipy.optimize.least_squares` |
80
+ | only a learned black-box surrogate (a neural network) + an expensive oracle | Bayesian optimization |
60
81
 
61
- - **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
62
- - **Quantum control (GRAPE)** — the single cleanest result across every domain tested
63
- - **Computer vision** — multi-camera bundle adjustment / camera calibration
64
- - **Finance** — portfolio optimization (Markowitz mean-variance)
65
- - **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
66
- - Pharmacokinetics, logistic regression, and more
82
+ ## What `calibrate=True` does (new in 0.7.0)
67
83
 
68
- ## Where it does *not* help
84
+ The model is evaluated on nested soft numbers: one soft axis along the probe direction, the other along each
85
+ uncertain parameter. The mixed part of the result is the exact sensitivity of the model's predicted slope to
86
+ that parameter, and every measured slope updates the parameters through it (a Kalman filter). Whatever the
87
+ parameters cannot explain is carried as an explicit discrepancy term on the soft axis. Each correction is
88
+ SoftOpt's bounded Newton step on the calibrated model, sized by how certain the calibrated prediction is, and the
89
+ calibrated model is evaluated at the target before the step is taken.
69
90
 
70
- 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.
91
+ `calibrate=True` also works on a plain `model(theta)` with no bias parameters; naming the uncertain parameters is
92
+ what makes it reliable across domains.
71
93
 
72
- **Barren plateaus.** In a deep hardware-efficient circuit at random
73
- initialisation, the gradient decays exponentially in the qubit count and the
74
- shot-noise measurement of it is larger than the thing it measures. An exact
75
- derivative still helps there — it comes from the circuit, so it stays exact
76
- however flat the landscape gets — but it does not remove the barrier.
94
+ ## Results across domains
77
95
 
78
- We measured this at 4–10 qubits on a transverse-field Ising chain: SoftOpt
79
- covers **1.4–1.6× as much of the distance to the ground state** as Adam, and
80
- that ratio held across three seed streams, five shot budgets (512 to 65536),
81
- five SPSA probe sizes and three different Hamiltonians — 316 wins in 320 runs
82
- across 16 conditions. The advantage *grows* as the measurement degrades, which
83
- is the mechanism.
96
+ One small problem per domain, one protocol for all: the model is the domain's standard simulator with its
97
+ uncertain parameters offset at random, the real system also carries an effect the model does not have
98
+ (except in the exact-model control),
99
+ measurements are noisy, 200 measurements, 10 seeds, the same start, Adam at its best of five learning rates and
100
+ SoftOpt at Adam's. `benchmarks/softbench/`.
84
101
 
85
- But at 10 qubits both arms covered only a few percent of the way to the ground
86
- state. 1.5× of very little is still very little. **If the plateau is your
87
- problem, this is not the fix.** The scan and its replication are in
88
- `benchmarks_barren_plateau/`.
102
+ **Measurements needed to reach the quality Adam reaches with 200**
103
+
104
+ | domain | SoftOpt `calibrate=True` | runs that got there |
105
+ |---|---|---|
106
+ | Quantum chemistry (H₂) | **5.4× fewer** | 10/10 |
107
+ | Epidemic control (vaccination) | **4.3× fewer** | 10/10 |
108
+ | Drug dosing (pharmacokinetics) | **3.3× fewer** | 10/10 |
109
+ | Portfolio optimization | **3.0× fewer** | 10/10 |
110
+ | Exact-model control | **3.0× fewer** | 10/10 |
111
+ | RF beam steering (phased array) | **2.7× fewer** | 9/10 |
112
+ | Batch chemical reactor | **2.2× fewer** | 10/10 |
113
+ | Robot arm controller tuning | **1.9× fewer** | 8/10 |
114
+ | Classifier under data shift | **1.9× fewer** | 8/10 |
115
+ | Building thermal control | **1.8× fewer** | 6/10 |
116
+ | Battery fast charging | **1.35× fewer** | 7/10 |
117
+
118
+ **Remaining gap to the true optimum after 200 measurements** (fraction of the starting gap; median)
119
+
120
+ | domain | Adam | COBYLA | SoftOpt default | **SoftOpt `calibrate=True`** |
121
+ |---|---|---|---|---|
122
+ | Quantum chemistry (H₂) | 0.0010 | 0.012 | 0.0040 | **0.0001** |
123
+ | RF beam steering | 0.0043 | 0.053 | 0.079 | **0.0026** |
124
+ | Robot arm tuning | 0.0125 | 0.026 | 0.024 | **0.0027** |
125
+ | Batch reactor | 0.0065 | 0.14 | 0.029 | **0.0023** |
126
+ | Drug dosing | 0.0198 | 0.056 | 0.067 | **0.0016** |
127
+ | Portfolio | 0.052 | 0.10 | 0.68 | **0.030** |
128
+ | Battery charging | 0.032 | 0.17 | 1.38 | **0.023** |
129
+ | Building thermal control | 0.0089 | 0.069 | 0.046 | 0.0148 |
130
+ | Epidemic control | 0.0081 | 0.024 | ~0 | 0.0035 |
131
+ | Classifier under data shift | 0.22 | 0.49 | 0.105 | 0.23 |
132
+ | Exact-model control | 0.0026 | 0.0048 | 0.00009 | 0.00016 |
133
+
134
+ **Quantum chemistry with gate errors, on IBM's FakeFez noise model**
135
+
136
+ H₂ on a device whose gates carry coherent errors (over-rotations, phase offsets, residual coupling), so the
137
+ ideal circuit no longer points at the right parameters. 180 device readings, 20 new seeds:
138
+
139
+ | | energy above the ground state (mHa, median) |
140
+ |---|---|
141
+ | Adam | 4.7 |
142
+ | COBYLA | 2.6 |
143
+ | SoftOpt, default | 13.0 |
144
+ | **SoftOpt, `calibrate=True`** | **0.42** (better than Adam 20/20, than COBYLA 16/20) |
145
+
146
+ With gate errors twice as large: 0.48 mHa, against Adam 4.8 and COBYLA 2.1. On a two-qubit gate calibration
147
+ task (18 angles, FakeFez, hidden coherent errors) the calibrated layer reaches 1.4e-4 infidelity against
148
+ Adam's 3.7e-4 (18/20), and in sensorless adaptive optics 0.0023 against 0.0041 (17/20).
89
149
 
90
150
  ## Installation
91
151
 
@@ -298,10 +358,14 @@ The full soft-number algebra is available from the package (`from softopt import
298
358
 
299
359
  `SoftNumber` wraps these with operator syntax (`x + y`, `x * y`, `x ** 3`, `x.sqrt()`) and is verified, axiom by axiom, against the book's own proofs that bridge numbers form an abelian group under addition and a ring under both operations (`tests/test_soft_number.py`).
300
360
 
301
- ## What SoftOpt is *not*
361
+ ## Choosing between the modes
362
+
363
+ - `calibrate=True` — your model has parameters you are not sure of. The recommended starting point whenever you
364
+ measure a real system.
365
+ - default (`slope="model"`) — your model is exact; the measurements are only noisy.
366
+ - `slope="measured"` — the model is approximate and you do not want to name its uncertain parameters.
302
367
 
303
- - 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).
304
- - Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
368
+ `calibrate=True` is available in `SoftOpt` (numpy). `SoftOptTorch` runs the default correction.
305
369
 
306
370
  ## License
307
371
 
@@ -1,65 +1,125 @@
1
1
  # SoftOpt
2
2
 
3
- **A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
3
+ **An optimization layer that closes the gap between your model and the real system.**
4
4
 
5
- Each step is an update followed by an exact Newton correction computed from your own model, through Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024). Free, open source, and runs locally.
5
+ You have a model of your system — a simulator, a circuit, a physical law, a pharmacokinetic model — and you
6
+ measure the real thing, where every measurement costs time or money. SoftOpt sits on top of Adam and, on every
7
+ step, takes an exact Newton correction from your model through Klein–Maimon soft-number calculus (*Foundations
8
+ of Soft Logic*, Klein & Maimon, Springer 2024). With `calibrate=True` the same measurements also correct the
9
+ model's uncertain parameters while the optimization runs, so the correction keeps pointing at the real system's
10
+ optimum instead of the model's.
6
11
 
7
- ## Is SoftOpt the right tool for your problem?
12
+ **When your model is exact** — a quantum circuit, a known physical law — SoftOpt gets closer to the true optimum
13
+ than Adam and COBYLA. On H₂ on IBM's FakeFez noise model it reaches chemical accuracy in 10 of 10 runs against
14
+ COBYLA's 3 of 10 (0.11 vs 5.81 mHa, same 180 readings); on the H₄ molecule (8 qubits, 185 Pauli terms) its gap to
15
+ the ground state is 67% smaller than Adam's, 10/10; in two-qubit quantum control, 84% smaller, 20/20.
8
16
 
9
- One question decides it, and you can answer it without running anything.
17
+ **When your model is approximately right** — the usual case on real systems — `calibrate=True` closes the gap.
18
+ Across eleven domains — quantum chemistry, beam steering, robotics, chemical engineering, drug dosing, finance,
19
+ building control, batteries, epidemiology, machine learning and an exact-model control — SoftOpt with
20
+ `calibrate=True` reaches the quality Adam reaches after 200 measurements with **2.65× fewer measurements**
21
+ (median; 1.35× to 5.4× by domain).
10
22
 
11
- **Can you get a residual vector?** That is: can you compute
12
- `model(theta) - data` and get back an *array*, rather than a single number?
23
+ Free, open source, and runs locally.
13
24
 
14
- - **Yes** → use `scipy.optimize.least_squares`. It builds the full Jacobian and
15
- exploits the least-squares structure, and it will beat SoftOpt on both
16
- accuracy and speed. This holds regardless of how large or how noisy the
17
- problem is. We measured it: on solar-cell parameter extraction, `least_squares`
18
- reached 7.9 mA against SoftOpt's 37.3 mA, and did it 21x faster.
25
+ ## When to use it
19
26
 
20
- - **No** → SoftOpt is built for this case. Each evaluation returns **one number,
21
- and it is noisy**: a quantum measurement, a laser-tracker sweep, a backtest, a
22
- simulator that returns a score. There is no residual vector for
23
- `least_squares` to work with, and finite-difference methods like L-BFGS-B end
24
- up differentiating the noise rather than the model.
27
+ **You have a model of your system and you measure the real one?** Use `calibrate=True` and name the model
28
+ parameters you are not sure of, with a generous estimate of how far off they may be.
25
29
 
26
- SoftOpt's advantage comes from taking the derivative **from your model** instead
27
- of from your measurements. That only matters when the measurements are the noisy
28
- part. Every validated domain below is of the second kind; that is not a
29
- convenience of selection, it is where the mechanism applies.
30
+ ```python
31
+ from softopt import SoftOpt, soft_compile, spsa_gradient
32
+
33
+ def model(theta, beta): # your simulator; beta = offsets of its uncertain parameters (0 = nominal)
34
+ ...
35
+
36
+ g = soft_compile(model, n_params=n, n_bias=k)
37
+ opt = SoftOpt(n, g, lr=0.05, calibrate=True, bias_scale=0.2) # bias_scale: expected size of beta (generous)
38
+
39
+ for step in range(num_steps):
40
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 measurements of the real system
41
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
42
+
43
+ opt.calibration # the current estimates: beta, contrast, and the discrepancy the model cannot explain
44
+ ```
30
45
 
31
- ## Where it helps
46
+ Everything else stays as it was: the same Adam update, the same measurements, no extra readings. When unsure of
47
+ `bias_scale`, err on the wide side.
32
48
 
33
- 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 of that model on every step, plus the curvature along the same direction, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
49
+ | your situation | the tool |
50
+ |---|---|
51
+ | a model with uncertain parameters + measurements of the real system | **SoftOpt, `calibrate=True`** |
52
+ | an exact model + noisy measurements | SoftOpt, default settings |
53
+ | you can compute a residual vector `model(theta) - data` | `scipy.optimize.least_squares` |
54
+ | only a learned black-box surrogate (a neural network) + an expensive oracle | Bayesian optimization |
34
55
 
35
- - **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
36
- - **Quantum control (GRAPE)** — the single cleanest result across every domain tested
37
- - **Computer vision** — multi-camera bundle adjustment / camera calibration
38
- - **Finance** — portfolio optimization (Markowitz mean-variance)
39
- - **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
40
- - Pharmacokinetics, logistic regression, and more
56
+ ## What `calibrate=True` does (new in 0.7.0)
41
57
 
42
- ## Where it does *not* help
58
+ The model is evaluated on nested soft numbers: one soft axis along the probe direction, the other along each
59
+ uncertain parameter. The mixed part of the result is the exact sensitivity of the model's predicted slope to
60
+ that parameter, and every measured slope updates the parameters through it (a Kalman filter). Whatever the
61
+ parameters cannot explain is carried as an explicit discrepancy term on the soft axis. Each correction is
62
+ SoftOpt's bounded Newton step on the calibrated model, sized by how certain the calibrated prediction is, and the
63
+ calibrated model is evaluated at the target before the step is taken.
43
64
 
44
- 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.
65
+ `calibrate=True` also works on a plain `model(theta)` with no bias parameters; naming the uncertain parameters is
66
+ what makes it reliable across domains.
45
67
 
46
- **Barren plateaus.** In a deep hardware-efficient circuit at random
47
- initialisation, the gradient decays exponentially in the qubit count and the
48
- shot-noise measurement of it is larger than the thing it measures. An exact
49
- derivative still helps there — it comes from the circuit, so it stays exact
50
- however flat the landscape gets — but it does not remove the barrier.
68
+ ## Results across domains
51
69
 
52
- We measured this at 4–10 qubits on a transverse-field Ising chain: SoftOpt
53
- covers **1.4–1.6× as much of the distance to the ground state** as Adam, and
54
- that ratio held across three seed streams, five shot budgets (512 to 65536),
55
- five SPSA probe sizes and three different Hamiltonians — 316 wins in 320 runs
56
- across 16 conditions. The advantage *grows* as the measurement degrades, which
57
- is the mechanism.
70
+ One small problem per domain, one protocol for all: the model is the domain's standard simulator with its
71
+ uncertain parameters offset at random, the real system also carries an effect the model does not have
72
+ (except in the exact-model control),
73
+ measurements are noisy, 200 measurements, 10 seeds, the same start, Adam at its best of five learning rates and
74
+ SoftOpt at Adam's. `benchmarks/softbench/`.
58
75
 
59
- But at 10 qubits both arms covered only a few percent of the way to the ground
60
- state. 1.5× of very little is still very little. **If the plateau is your
61
- problem, this is not the fix.** The scan and its replication are in
62
- `benchmarks_barren_plateau/`.
76
+ **Measurements needed to reach the quality Adam reaches with 200**
77
+
78
+ | domain | SoftOpt `calibrate=True` | runs that got there |
79
+ |---|---|---|
80
+ | Quantum chemistry (H₂) | **5.4× fewer** | 10/10 |
81
+ | Epidemic control (vaccination) | **4.3× fewer** | 10/10 |
82
+ | Drug dosing (pharmacokinetics) | **3.3× fewer** | 10/10 |
83
+ | Portfolio optimization | **3.0× fewer** | 10/10 |
84
+ | Exact-model control | **3.0× fewer** | 10/10 |
85
+ | RF beam steering (phased array) | **2.7× fewer** | 9/10 |
86
+ | Batch chemical reactor | **2.2× fewer** | 10/10 |
87
+ | Robot arm controller tuning | **1.9× fewer** | 8/10 |
88
+ | Classifier under data shift | **1.9× fewer** | 8/10 |
89
+ | Building thermal control | **1.8× fewer** | 6/10 |
90
+ | Battery fast charging | **1.35× fewer** | 7/10 |
91
+
92
+ **Remaining gap to the true optimum after 200 measurements** (fraction of the starting gap; median)
93
+
94
+ | domain | Adam | COBYLA | SoftOpt default | **SoftOpt `calibrate=True`** |
95
+ |---|---|---|---|---|
96
+ | Quantum chemistry (H₂) | 0.0010 | 0.012 | 0.0040 | **0.0001** |
97
+ | RF beam steering | 0.0043 | 0.053 | 0.079 | **0.0026** |
98
+ | Robot arm tuning | 0.0125 | 0.026 | 0.024 | **0.0027** |
99
+ | Batch reactor | 0.0065 | 0.14 | 0.029 | **0.0023** |
100
+ | Drug dosing | 0.0198 | 0.056 | 0.067 | **0.0016** |
101
+ | Portfolio | 0.052 | 0.10 | 0.68 | **0.030** |
102
+ | Battery charging | 0.032 | 0.17 | 1.38 | **0.023** |
103
+ | Building thermal control | 0.0089 | 0.069 | 0.046 | 0.0148 |
104
+ | Epidemic control | 0.0081 | 0.024 | ~0 | 0.0035 |
105
+ | Classifier under data shift | 0.22 | 0.49 | 0.105 | 0.23 |
106
+ | Exact-model control | 0.0026 | 0.0048 | 0.00009 | 0.00016 |
107
+
108
+ **Quantum chemistry with gate errors, on IBM's FakeFez noise model**
109
+
110
+ H₂ on a device whose gates carry coherent errors (over-rotations, phase offsets, residual coupling), so the
111
+ ideal circuit no longer points at the right parameters. 180 device readings, 20 new seeds:
112
+
113
+ | | energy above the ground state (mHa, median) |
114
+ |---|---|
115
+ | Adam | 4.7 |
116
+ | COBYLA | 2.6 |
117
+ | SoftOpt, default | 13.0 |
118
+ | **SoftOpt, `calibrate=True`** | **0.42** (better than Adam 20/20, than COBYLA 16/20) |
119
+
120
+ With gate errors twice as large: 0.48 mHa, against Adam 4.8 and COBYLA 2.1. On a two-qubit gate calibration
121
+ task (18 angles, FakeFez, hidden coherent errors) the calibrated layer reaches 1.4e-4 infidelity against
122
+ Adam's 3.7e-4 (18/20), and in sensorless adaptive optics 0.0023 against 0.0041 (17/20).
63
123
 
64
124
  ## Installation
65
125
 
@@ -272,10 +332,14 @@ The full soft-number algebra is available from the package (`from softopt import
272
332
 
273
333
  `SoftNumber` wraps these with operator syntax (`x + y`, `x * y`, `x ** 3`, `x.sqrt()`) and is verified, axiom by axiom, against the book's own proofs that bridge numbers form an abelian group under addition and a ring under both operations (`tests/test_soft_number.py`).
274
334
 
275
- ## What SoftOpt is *not*
335
+ ## Choosing between the modes
336
+
337
+ - `calibrate=True` — your model has parameters you are not sure of. The recommended starting point whenever you
338
+ measure a real system.
339
+ - default (`slope="model"`) — your model is exact; the measurements are only noisy.
340
+ - `slope="measured"` — the model is approximate and you do not want to name its uncertain parameters.
276
341
 
277
- - 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).
278
- - Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
342
+ `calibrate=True` is available in `SoftOpt` (numpy). `SoftOptTorch` runs the default correction.
279
343
 
280
344
  ## License
281
345
 
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "softopt"
7
- version = "0.6.4"
8
- description = "A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus"
7
+ version = "0.7.0"
8
+ description = "An optimization layer that closes the gap between your model and the real system, built on Klein–Maimon soft-number calculus"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.9"
12
12
  authors = [
13
13
  { name = "Ido Angel" }
14
14
  ]
15
- keywords = ["optimizer", "optimization", "soft-logic", "soft-numbers", "uncertainty", "sensitivity", "quantum-computing", "vqe"]
15
+ keywords = ["optimizer", "optimization", "soft-logic", "soft-numbers", "uncertainty", "sensitivity", "calibration", "digital-twin", "sim-to-real", "quantum-computing", "vqe"]
16
16
  classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Intended Audience :: Science/Research",
@@ -1,16 +1,16 @@
1
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, ...).
2
+ SoftOpt — an optimization layer built on Klein–Maimon soft-number calculus that closes the gap between your
3
+ model (a simulator, a circuit, a physical law) and the real system you measure.
5
4
 
6
- from softopt import SoftOpt # numpy version (slope="model" or "measured")
5
+ from softopt import SoftOpt # numpy version (slope="model" or "measured"; calibrate=True)
7
6
  from softopt import spsa_gradient # gradient, direction and measured slope from 2 readings
8
7
  from softopt import SoftOptTorch # PyTorch version (torch.optim.Optimizer)
9
8
  from softopt import soft_compile # turn a plain-Python model into g_delta
10
9
  from softopt import SoftNumber # the algebraic primitive itself
11
10
 
12
- See README.md for the exact requirement your problem must satisfy
13
- (the "known computation graph" criterion) and worked examples.
11
+ calibrate=True (new in 0.7.0): soft_compile(model, n_params, n_bias=k) for model(theta, beta), then
12
+ SoftOpt(..., calibrate=True, bias_scale=s) -- the measurements also calibrate the model's uncertain parameters.
13
+ See README.md.
14
14
  """
15
15
  from .numpy_optimizer import SoftOpt, spsa_gradient
16
16
  from .compile import soft_compile
@@ -33,4 +33,4 @@ __all__ = [
33
33
  "sadd", "sneg", "ssub", "smul", "sinv", "sdiv",
34
34
  "spow", "sroot", "ssqrt", "ssin", "scos", "sexp", "slog",
35
35
  ]
36
- __version__ = "0.6.4"
36
+ __version__ = "0.7.0"
@@ -0,0 +1,159 @@
1
+ """
2
+ Online calibration of the model against the real system (calibrate=True).
3
+
4
+ The model you give SoftOpt is rarely exactly the system you are optimising: a simulator with approximate
5
+ parameters, an ideal circuit on a device with gate errors, a nominal plant. With calibrate=True every SPSA
6
+ pair you already measure is also used to correct the model while the optimisation runs -- no extra readings.
7
+
8
+ What the soft algebra does here
9
+ The model is evaluated on nested soft numbers: one axis (eps1) along the probe direction d, the other
10
+ (eps2) along a bias parameter beta_j. The mixed eps1*eps2 part of the result is d D1 / d beta_j exactly --
11
+ how the model's predicted slope responds to each uncertain parameter. That is the measurement Jacobian of a
12
+ Kalman filter on beta: each measured slope s along d is explained as
13
+ s ~ kappa * ( D1_model(x, d; beta) + dg . d )
14
+ kappa is the contrast the device loses (noise), and dg is the part of the slope the model cannot explain at
15
+ all (its eps-part is d itself, so it costs nothing). Two filters run on the same readings -- dg local
16
+ (forgotten at 0.95) and dg settled (not forgotten) -- and the one that has predicted the readings better so
17
+ far (log-likelihood) supplies the correction.
18
+
19
+ The correction is SoftOpt's bounded Newton step on the CALIBRATED model, scaled by the filter's own
20
+ confidence D1^2 / (D1^2 + var D1), and checked before it is taken: the calibrated model is evaluated at the
21
+ target, and if it predicts no improvement the step is shortened to the minimum of the parabola through
22
+ f(0), D1 and f(step).
23
+
24
+ Two ways to use it
25
+ * calibrate=True with a plain model(theta): no bias parameters; only kappa and dg are learned.
26
+ * soft_compile(model(theta, beta), n_params, n_bias=k) and SoftOpt(..., calibrate=True, bias_scale=s):
27
+ beta are the model's uncertain parameters (offsets from their nominal values, 0 = nominal); bias_scale is
28
+ their expected size (a number or one per parameter), on the PHYSICAL scale -- a prior far too wide lets the
29
+ filter fit noise.
30
+
31
+ Validated (development + held-out seeds) where the bias structure is right: gate calibration, adaptive optics,
32
+ H2 with coherent gate errors, robot sim-to-real. When the model lacks a major error type the median is still at
33
+ or below Adam but single runs can be worse; when the model is already exact, calibrate=False is better.
34
+ """
35
+ import numpy as np
36
+
37
+ from .soft_number import SoftNumber
38
+
39
+
40
+ def _sn(e, a, c, v):
41
+ """v + c*eps_in + a*eps_out + e*eps_in*eps_out as a nested SoftNumber."""
42
+ return SoftNumber(SoftNumber(e, a), SoftNumber(c, v))
43
+
44
+
45
+ def _parts(r):
46
+ if isinstance(r, SoftNumber):
47
+ outer_a, outer_b = r.a, r.b
48
+ e, a = (outer_a.a, outer_a.b) if isinstance(outer_a, SoftNumber) else (0.0, outer_a)
49
+ c, v = (outer_b.a, outer_b.b) if isinstance(outer_b, SoftNumber) else (0.0, outer_b)
50
+ return float(v), float(c), float(a), float(e)
51
+ return float(r), 0.0, 0.0, 0.0
52
+
53
+
54
+ class CompiledModel:
55
+ """model_fn(theta) or model_fn(theta, beta) on nested soft numbers: D1/D2 along d, d D1 / d beta, value."""
56
+
57
+ def __init__(self, model_fn, n_bias=0):
58
+ self.f, self.nb = model_fn, int(n_bias)
59
+
60
+ def _call(self, th, bs):
61
+ return self.f(th, bs) if self.nb else self.f(th)
62
+
63
+ def d1_d2(self, x, d, beta):
64
+ th = [_sn(0.0, float(di), float(di), float(xi)) for xi, di in zip(x, d)]
65
+ bs = [_sn(0.0, 0.0, 0.0, float(b)) for b in beta]
66
+ _, D1, _, D2 = _parts(self._call(th, bs))
67
+ return D1, D2
68
+
69
+ def d1_and_jac(self, x, d, beta):
70
+ th = [_sn(0.0, 0.0, float(di), float(xi)) for xi, di in zip(x, d)] # eps_in along d
71
+ jac, D1 = np.zeros(self.nb), None
72
+ for j in range(self.nb):
73
+ bs = [_sn(0.0, 1.0 if i == j else 0.0, 0.0, float(b)) for i, b in enumerate(beta)] # eps_out along beta_j
74
+ _, D1, _, jac[j] = _parts(self._call(th, bs))
75
+ if D1 is None:
76
+ _, D1, _, _ = _parts(self._call(th, []))
77
+ return D1, jac
78
+
79
+ def value(self, x, beta):
80
+ th = [_sn(0.0, 0.0, 0.0, float(xi)) for xi in x]
81
+ bs = [_sn(0.0, 0.0, 0.0, float(b)) for b in beta]
82
+ return _parts(self._call(th, bs))[0]
83
+
84
+
85
+ class Calibrator:
86
+ FORGETS = (0.95, 1.0)
87
+
88
+ def __init__(self, n, model, bias_scale=None):
89
+ self.n, self.M = n, model
90
+ nb = model.nb
91
+ if nb and bias_scale is None:
92
+ raise ValueError('calibrate=True with bias parameters needs bias_scale= (their expected size, '
93
+ 'on the physical scale)')
94
+ self.ib, self.ik, self.ig = slice(0, nb), nb, slice(nb + 1, nb + 1 + n)
95
+ self.F = []
96
+ for fg in self.FORGETS:
97
+ st = np.zeros(nb + 1 + n); st[nb] = 1.0
98
+ P = np.zeros((nb + 1 + n,) * 2)
99
+ if nb:
100
+ P[self.ib, self.ib] = np.diag(np.broadcast_to(np.asarray(bias_scale, float) ** 2, (nb,)))
101
+ P[nb, nb] = 0.2 ** 2
102
+ self.F.append(dict(forget=fg, state=st, P=P, r2=None, ll=0.0))
103
+ self.t = 0
104
+
105
+ def update(self, x, d, slope):
106
+ """One measured slope along d at x: update both filters and their evidence."""
107
+ ib, ik, ig, n = self.ib, self.ik, self.ig, self.n
108
+ for f in self.F:
109
+ st, P = f['state'], f['P']
110
+ D1, jac = self.M.d1_and_jac(x, d, st[ib])
111
+ pred = D1 + st[ig] @ d
112
+ innov = slope - st[ik] * pred
113
+ if f['r2'] is None:
114
+ f['r2'] = innov ** 2
115
+ P[ig, ig] = np.eye(n) * (f['r2'] / n)
116
+ else:
117
+ f['r2'] = 0.95 * f['r2'] + 0.05 * innov ** 2
118
+ P[ig, :] /= np.sqrt(f['forget']); P[:, ig] /= np.sqrt(f['forget'])
119
+ H = np.r_[st[ik] * jac, pred, st[ik] * d]
120
+ S = H @ P @ H + f['r2']
121
+ if S <= 0 or not np.isfinite(S):
122
+ continue
123
+ if self.t > 0:
124
+ f['ll'] += -0.5 * (np.log(S) + innov ** 2 / S)
125
+ K = P @ H / S
126
+ f['state'] = st + K * innov
127
+ f['P'] = P - np.outer(K, H @ P)
128
+ self.t += 1
129
+
130
+ def best(self):
131
+ return self.F[int(np.argmax([f['ll'] for f in self.F]))]
132
+
133
+ def correction(self, x, e, lo, hi, eta_fallback):
134
+ f = self.best(); st, P = f['state'], f['P']
135
+ beta, dg = st[self.ib], st[self.ig]
136
+ D1m, D2m = self.M.d1_d2(x, e, beta)
137
+ _, jac_e = self.M.d1_and_jac(x, e, beta)
138
+ D1 = D1m + dg @ e
139
+ h = np.r_[jac_e, 0.0, e]
140
+ var = max(float(h @ P @ h), 0.0)
141
+ t = -D1 / D2m if D2m > 1e-8 else -eta_fallback * D1
142
+ w = D1 ** 2 / (D1 ** 2 + var) if (D1 ** 2 + var) > 0 else 0.0
143
+ step = float(np.clip(w * t, lo, hi))
144
+ if step != 0.0:
145
+ f0 = self.M.value(x, beta)
146
+ for _ in range(20):
147
+ df = self.M.value(x + step * e, beta) - f0 + step * (dg @ e)
148
+ if df < 0:
149
+ break
150
+ c = (df - D1 * step) / step ** 2
151
+ new = -D1 / (2 * c) if c > 0 else 0.5 * step
152
+ step = new if 0 < new / step < 1 else 0.5 * step
153
+ return step
154
+
155
+ @property
156
+ def state(self):
157
+ f = self.best(); st = f['state']
158
+ return dict(beta=st[self.ib].copy(), kappa=float(st[self.ik]), discrepancy=st[self.ig].copy(),
159
+ local_discrepancy=f['forget'] < 1.0)
@@ -27,13 +27,22 @@ import numpy as np
27
27
  from .soft_number import SoftNumber
28
28
 
29
29
 
30
- def soft_compile(model_fn, n_params=None, verify=True, verify_tol=1e-5):
30
+ def soft_compile(model_fn, n_params=None, verify=True, verify_tol=1e-5, n_bias=0):
31
31
  """model_fn: takes a sequence of scalars, returns a scalar.
32
+ n_bias: for calibrate=True with bias parameters -- model_fn(theta, beta) then takes a second sequence of
33
+ n_bias uncertain model parameters (offsets from nominal, 0 = nominal); SoftOpt itself sees the nominal
34
+ model model_fn(theta, [0]*n_bias). See softopt.calibrate.
32
35
  n_params: needed only for verification.
33
36
  verify: check the compiled derivative against finite differences at a
34
37
  random point before returning it. Catches the common mistakes --
35
38
  a model that isn't actually differentiable, uses an unsupported
36
39
  operation, or silently drops the traced parameters."""
40
+ from .calibrate import CompiledModel
41
+ calibration = CompiledModel(model_fn, n_bias)
42
+ if n_bias:
43
+ _full = model_fn
44
+ model_fn = lambda theta: _full(theta, [0.0] * int(n_bias)) # noqa: E731 (nominal model)
45
+
37
46
  def g_delta(theta, delta):
38
47
  soft = [SoftNumber(float(d), float(t)) for t, d in zip(theta, delta)]
39
48
  result = model_fn(soft)
@@ -88,6 +97,8 @@ def soft_compile(model_fn, n_params=None, verify=True, verify_tol=1e-5):
88
97
  except Exception:
89
98
  pass
90
99
 
100
+ g_delta.calibration = calibration
101
+
91
102
  if verify:
92
103
  if n_params is None:
93
104
  raise ValueError("pass n_params to verify the compiled model, "
@@ -38,7 +38,8 @@ import numpy as np
38
38
  class SoftOpt:
39
39
  def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
40
40
  newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
41
- h_fd=1e-4, seed=None, slope="model", exact_d2="auto", _test_frozen_constant=1.0):
41
+ h_fd=1e-4, seed=None, slope="model", exact_d2="auto", calibrate=False, bias_scale=None,
42
+ _test_frozen_constant=1.0):
42
43
  if slope not in ("model", "measured"):
43
44
  raise ValueError(f'slope must be "model" or "measured", got {slope!r}')
44
45
  self.slope = slope
@@ -59,6 +60,14 @@ class SoftOpt:
59
60
  self.v = None
60
61
  self.t = 0
61
62
  self.rng = np.random.default_rng(seed)
63
+ self.calibrator = None
64
+ if calibrate:
65
+ model = getattr(g_delta, "calibration", None)
66
+ if model is None:
67
+ raise ValueError("calibrate=True needs a g_delta from soft_compile (it carries the model the "
68
+ "calibration evaluates on nested soft numbers)")
69
+ from .calibrate import Calibrator
70
+ self.calibrator = Calibrator(n_params, model, bias_scale)
62
71
  self._ablation_rng = None
63
72
  self._test_frozen_constant = _test_frozen_constant
64
73
 
@@ -87,6 +96,8 @@ class SoftOpt:
87
96
  """
88
97
  theta = np.asarray(theta, dtype=float)
89
98
  grad_estimate = np.asarray(grad_estimate, dtype=float)
99
+ if self.calibrator is not None:
100
+ return self._calibrated_step(theta, grad_estimate, direction, measured_slope)
90
101
  if self.slope == "measured":
91
102
  if direction is None or measured_slope is None:
92
103
  raise ValueError('slope="measured" needs direction= and measured_slope= on every '
@@ -160,6 +171,34 @@ class SoftOpt:
160
171
  return theta_after_adam + t_star * delta
161
172
 
162
173
 
174
+ def _calibrated_step(self, theta, grad_estimate, direction, measured_slope):
175
+ """calibrate=True: the measured pair also calibrates the model (softopt.calibrate); the Adam update is
176
+ unchanged; the correction is the bounded Newton step of the CALIBRATED model."""
177
+ if direction is None or measured_slope is None:
178
+ raise ValueError("calibrate=True needs direction= and measured_slope= on every step "
179
+ "(spsa_gradient() returns both)")
180
+ direction = np.asarray(direction, dtype=float)
181
+ self.calibrator.update(theta, direction, float(measured_slope))
182
+ self.t += 1
183
+ if self.m is None:
184
+ self.m = np.zeros_like(theta)
185
+ self.v = np.zeros_like(theta)
186
+ self.m = self.b1 * self.m + (1 - self.b1) * grad_estimate
187
+ self.v = self.b2 * self.v + (1 - self.b2) * grad_estimate ** 2
188
+ mh = self.m / (1 - self.b1 ** self.t)
189
+ vh = self.v / (1 - self.b2 ** self.t)
190
+ x = theta - self.lr * mh / (np.sqrt(vh) + self.eps)
191
+ e = self.rng.choice([-1.0, 1.0], size=theta.shape[0])
192
+ step = self.calibrator.correction(x, e, self.newton_lo, self.newton_hi, self.eta_fallback)
193
+ return x + step * e
194
+
195
+ @property
196
+ def calibration(self):
197
+ """calibrate=True: current estimates -- beta (bias parameters), kappa (contrast), the discrepancy
198
+ gradient, and which of the two discrepancy filters is in use."""
199
+ return None if self.calibrator is None else self.calibrator.state
200
+
201
+
163
202
  def spsa_gradient(measure, theta, c=0.1, rng=None):
164
203
  """One SPSA probe of a noisy objective.
165
204
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.6.4
4
- Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
3
+ Version: 0.7.0
4
+ Summary: An optimization layer that closes the gap between your model and the real system, built on Klein–Maimon soft-number calculus
5
5
  Author: Ido Angel
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/mobiuai/softopt
8
8
  Project-URL: Repository, https://github.com/mobiuai/softopt
9
9
  Project-URL: Issues, https://github.com/mobiuai/softopt/issues
10
- Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,quantum-computing,vqe
10
+ Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,calibration,digital-twin,sim-to-real,quantum-computing,vqe
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -26,66 +26,126 @@ Dynamic: license-file
26
26
 
27
27
  # SoftOpt
28
28
 
29
- **A standalone optimizer that excels against leading market optimizers like Adam, in problems with a known, differentiable computation graph.**
29
+ **An optimization layer that closes the gap between your model and the real system.**
30
30
 
31
- Each step is an update followed by an exact Newton correction computed from your own model, through Klein–Maimon soft-number calculus (*Foundations of Soft Logic*, Klein & Maimon, Springer 2024). Free, open source, and runs locally.
31
+ You have a model of your system — a simulator, a circuit, a physical law, a pharmacokinetic model — and you
32
+ measure the real thing, where every measurement costs time or money. SoftOpt sits on top of Adam and, on every
33
+ step, takes an exact Newton correction from your model through Klein–Maimon soft-number calculus (*Foundations
34
+ of Soft Logic*, Klein & Maimon, Springer 2024). With `calibrate=True` the same measurements also correct the
35
+ model's uncertain parameters while the optimization runs, so the correction keeps pointing at the real system's
36
+ optimum instead of the model's.
32
37
 
33
- ## Is SoftOpt the right tool for your problem?
38
+ **When your model is exact** — a quantum circuit, a known physical law — SoftOpt gets closer to the true optimum
39
+ than Adam and COBYLA. On H₂ on IBM's FakeFez noise model it reaches chemical accuracy in 10 of 10 runs against
40
+ COBYLA's 3 of 10 (0.11 vs 5.81 mHa, same 180 readings); on the H₄ molecule (8 qubits, 185 Pauli terms) its gap to
41
+ the ground state is 67% smaller than Adam's, 10/10; in two-qubit quantum control, 84% smaller, 20/20.
34
42
 
35
- One question decides it, and you can answer it without running anything.
43
+ **When your model is approximately right** — the usual case on real systems — `calibrate=True` closes the gap.
44
+ Across eleven domains — quantum chemistry, beam steering, robotics, chemical engineering, drug dosing, finance,
45
+ building control, batteries, epidemiology, machine learning and an exact-model control — SoftOpt with
46
+ `calibrate=True` reaches the quality Adam reaches after 200 measurements with **2.65× fewer measurements**
47
+ (median; 1.35× to 5.4× by domain).
36
48
 
37
- **Can you get a residual vector?** That is: can you compute
38
- `model(theta) - data` and get back an *array*, rather than a single number?
49
+ Free, open source, and runs locally.
39
50
 
40
- - **Yes** → use `scipy.optimize.least_squares`. It builds the full Jacobian and
41
- exploits the least-squares structure, and it will beat SoftOpt on both
42
- accuracy and speed. This holds regardless of how large or how noisy the
43
- problem is. We measured it: on solar-cell parameter extraction, `least_squares`
44
- reached 7.9 mA against SoftOpt's 37.3 mA, and did it 21x faster.
51
+ ## When to use it
45
52
 
46
- - **No** → SoftOpt is built for this case. Each evaluation returns **one number,
47
- and it is noisy**: a quantum measurement, a laser-tracker sweep, a backtest, a
48
- simulator that returns a score. There is no residual vector for
49
- `least_squares` to work with, and finite-difference methods like L-BFGS-B end
50
- up differentiating the noise rather than the model.
53
+ **You have a model of your system and you measure the real one?** Use `calibrate=True` and name the model
54
+ parameters you are not sure of, with a generous estimate of how far off they may be.
51
55
 
52
- SoftOpt's advantage comes from taking the derivative **from your model** instead
53
- of from your measurements. That only matters when the measurements are the noisy
54
- part. Every validated domain below is of the second kind; that is not a
55
- convenience of selection, it is where the mechanism applies.
56
+ ```python
57
+ from softopt import SoftOpt, soft_compile, spsa_gradient
58
+
59
+ def model(theta, beta): # your simulator; beta = offsets of its uncertain parameters (0 = nominal)
60
+ ...
61
+
62
+ g = soft_compile(model, n_params=n, n_bias=k)
63
+ opt = SoftOpt(n, g, lr=0.05, calibrate=True, bias_scale=0.2) # bias_scale: expected size of beta (generous)
64
+
65
+ for step in range(num_steps):
66
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 measurements of the real system
67
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
68
+
69
+ opt.calibration # the current estimates: beta, contrast, and the discrepancy the model cannot explain
70
+ ```
56
71
 
57
- ## Where it helps
72
+ Everything else stays as it was: the same Adam update, the same measurements, no extra readings. When unsure of
73
+ `bias_scale`, err on the wide side.
58
74
 
59
- 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 of that model on every step, plus the curvature along the same direction, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
75
+ | your situation | the tool |
76
+ |---|---|
77
+ | a model with uncertain parameters + measurements of the real system | **SoftOpt, `calibrate=True`** |
78
+ | an exact model + noisy measurements | SoftOpt, default settings |
79
+ | you can compute a residual vector `model(theta) - data` | `scipy.optimize.least_squares` |
80
+ | only a learned black-box surrogate (a neural network) + an expensive oracle | Bayesian optimization |
60
81
 
61
- - **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
62
- - **Quantum control (GRAPE)** — the single cleanest result across every domain tested
63
- - **Computer vision** — multi-camera bundle adjustment / camera calibration
64
- - **Finance** — portfolio optimization (Markowitz mean-variance)
65
- - **Condensed-matter physics** — quasicrystal and spin-chain models (Ising, XY, Heisenberg, SSH, Kitaev)
66
- - Pharmacokinetics, logistic regression, and more
82
+ ## What `calibrate=True` does (new in 0.7.0)
67
83
 
68
- ## Where it does *not* help
84
+ The model is evaluated on nested soft numbers: one soft axis along the probe direction, the other along each
85
+ uncertain parameter. The mixed part of the result is the exact sensitivity of the model's predicted slope to
86
+ that parameter, and every measured slope updates the parameters through it (a Kalman filter). Whatever the
87
+ parameters cannot explain is carried as an explicit discrepancy term on the soft axis. Each correction is
88
+ SoftOpt's bounded Newton step on the calibrated model, sized by how certain the calibrated prediction is, and the
89
+ calibrated model is evaluated at the target before the step is taken.
69
90
 
70
- 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.
91
+ `calibrate=True` also works on a plain `model(theta)` with no bias parameters; naming the uncertain parameters is
92
+ what makes it reliable across domains.
71
93
 
72
- **Barren plateaus.** In a deep hardware-efficient circuit at random
73
- initialisation, the gradient decays exponentially in the qubit count and the
74
- shot-noise measurement of it is larger than the thing it measures. An exact
75
- derivative still helps there — it comes from the circuit, so it stays exact
76
- however flat the landscape gets — but it does not remove the barrier.
94
+ ## Results across domains
77
95
 
78
- We measured this at 4–10 qubits on a transverse-field Ising chain: SoftOpt
79
- covers **1.4–1.6× as much of the distance to the ground state** as Adam, and
80
- that ratio held across three seed streams, five shot budgets (512 to 65536),
81
- five SPSA probe sizes and three different Hamiltonians — 316 wins in 320 runs
82
- across 16 conditions. The advantage *grows* as the measurement degrades, which
83
- is the mechanism.
96
+ One small problem per domain, one protocol for all: the model is the domain's standard simulator with its
97
+ uncertain parameters offset at random, the real system also carries an effect the model does not have
98
+ (except in the exact-model control),
99
+ measurements are noisy, 200 measurements, 10 seeds, the same start, Adam at its best of five learning rates and
100
+ SoftOpt at Adam's. `benchmarks/softbench/`.
84
101
 
85
- But at 10 qubits both arms covered only a few percent of the way to the ground
86
- state. 1.5× of very little is still very little. **If the plateau is your
87
- problem, this is not the fix.** The scan and its replication are in
88
- `benchmarks_barren_plateau/`.
102
+ **Measurements needed to reach the quality Adam reaches with 200**
103
+
104
+ | domain | SoftOpt `calibrate=True` | runs that got there |
105
+ |---|---|---|
106
+ | Quantum chemistry (H₂) | **5.4× fewer** | 10/10 |
107
+ | Epidemic control (vaccination) | **4.3× fewer** | 10/10 |
108
+ | Drug dosing (pharmacokinetics) | **3.3× fewer** | 10/10 |
109
+ | Portfolio optimization | **3.0× fewer** | 10/10 |
110
+ | Exact-model control | **3.0× fewer** | 10/10 |
111
+ | RF beam steering (phased array) | **2.7× fewer** | 9/10 |
112
+ | Batch chemical reactor | **2.2× fewer** | 10/10 |
113
+ | Robot arm controller tuning | **1.9× fewer** | 8/10 |
114
+ | Classifier under data shift | **1.9× fewer** | 8/10 |
115
+ | Building thermal control | **1.8× fewer** | 6/10 |
116
+ | Battery fast charging | **1.35× fewer** | 7/10 |
117
+
118
+ **Remaining gap to the true optimum after 200 measurements** (fraction of the starting gap; median)
119
+
120
+ | domain | Adam | COBYLA | SoftOpt default | **SoftOpt `calibrate=True`** |
121
+ |---|---|---|---|---|
122
+ | Quantum chemistry (H₂) | 0.0010 | 0.012 | 0.0040 | **0.0001** |
123
+ | RF beam steering | 0.0043 | 0.053 | 0.079 | **0.0026** |
124
+ | Robot arm tuning | 0.0125 | 0.026 | 0.024 | **0.0027** |
125
+ | Batch reactor | 0.0065 | 0.14 | 0.029 | **0.0023** |
126
+ | Drug dosing | 0.0198 | 0.056 | 0.067 | **0.0016** |
127
+ | Portfolio | 0.052 | 0.10 | 0.68 | **0.030** |
128
+ | Battery charging | 0.032 | 0.17 | 1.38 | **0.023** |
129
+ | Building thermal control | 0.0089 | 0.069 | 0.046 | 0.0148 |
130
+ | Epidemic control | 0.0081 | 0.024 | ~0 | 0.0035 |
131
+ | Classifier under data shift | 0.22 | 0.49 | 0.105 | 0.23 |
132
+ | Exact-model control | 0.0026 | 0.0048 | 0.00009 | 0.00016 |
133
+
134
+ **Quantum chemistry with gate errors, on IBM's FakeFez noise model**
135
+
136
+ H₂ on a device whose gates carry coherent errors (over-rotations, phase offsets, residual coupling), so the
137
+ ideal circuit no longer points at the right parameters. 180 device readings, 20 new seeds:
138
+
139
+ | | energy above the ground state (mHa, median) |
140
+ |---|---|
141
+ | Adam | 4.7 |
142
+ | COBYLA | 2.6 |
143
+ | SoftOpt, default | 13.0 |
144
+ | **SoftOpt, `calibrate=True`** | **0.42** (better than Adam 20/20, than COBYLA 16/20) |
145
+
146
+ With gate errors twice as large: 0.48 mHa, against Adam 4.8 and COBYLA 2.1. On a two-qubit gate calibration
147
+ task (18 angles, FakeFez, hidden coherent errors) the calibrated layer reaches 1.4e-4 infidelity against
148
+ Adam's 3.7e-4 (18/20), and in sensorless adaptive optics 0.0023 against 0.0041 (17/20).
89
149
 
90
150
  ## Installation
91
151
 
@@ -298,10 +358,14 @@ The full soft-number algebra is available from the package (`from softopt import
298
358
 
299
359
  `SoftNumber` wraps these with operator syntax (`x + y`, `x * y`, `x ** 3`, `x.sqrt()`) and is verified, axiom by axiom, against the book's own proofs that bridge numbers form an abelian group under addition and a ring under both operations (`tests/test_soft_number.py`).
300
360
 
301
- ## What SoftOpt is *not*
361
+ ## Choosing between the modes
362
+
363
+ - `calibrate=True` — your model has parameters you are not sure of. The recommended starting point whenever you
364
+ measure a real system.
365
+ - default (`slope="model"`) — your model is exact; the measurements are only noisy.
366
+ - `slope="measured"` — the model is approximate and you do not want to name its uncertain parameters.
302
367
 
303
- - 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).
304
- - Not a general-purpose black-box optimizer — it requires a known computation graph, as described above.
368
+ `calibrate=True` is available in `SoftOpt` (numpy). `SoftOptTorch` runs the default correction.
305
369
 
306
370
  ## License
307
371
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  softopt/__init__.py
5
+ softopt/calibrate.py
5
6
  softopt/compile.py
6
7
  softopt/numpy_optimizer.py
7
8
  softopt/soft_number.py
@@ -12,6 +13,7 @@ softopt.egg-info/dependency_links.txt
12
13
  softopt.egg-info/requires.txt
13
14
  softopt.egg-info/top_level.txt
14
15
  tests/test_basic.py
16
+ tests/test_calibrate.py
15
17
  tests/test_causal.py
16
18
  tests/test_compile.py
17
19
  tests/test_exact_d2.py
@@ -0,0 +1,86 @@
1
+ """calibrate=True: the soft Jacobian is exact, the default path is unchanged, and calibration helps on a
2
+ biased model."""
3
+ import importlib.util
4
+ import os
5
+ import numpy as np
6
+ import pytest
7
+ from softopt import SoftOpt, soft_compile
8
+
9
+ N = 4
10
+ A = np.array([1.0, 2.0, 0.5, 1.5]); CC = np.array([0.3, -0.2, 0.5, 0.1])
11
+
12
+
13
+ def model(th, be):
14
+ return sum(A[i] * (th[i] - CC[i] - be[i]) ** 2 + 0.3 * np.cos(th[i] + be[i]) for i in range(N))
15
+
16
+
17
+ def test_soft_jacobian_matches_finite_differences():
18
+ cm = soft_compile(model, n_params=N, n_bias=N).calibration
19
+ rng = np.random.default_rng(0)
20
+ x, d, b = rng.normal(size=N), rng.choice([-1.0, 1.0], N), rng.normal(0, 0.2, N)
21
+ D1, jac = cm.d1_and_jac(x, d, b); h = 1e-6
22
+ for j in range(N):
23
+ e = np.zeros(N); e[j] = h
24
+ fd = (cm.d1_d2(x, d, b + e)[0] - cm.d1_d2(x, d, b - e)[0]) / (2 * h)
25
+ assert abs(jac[j] - fd) < 1e-6 * max(1, abs(fd))
26
+ fd1 = (cm.value(x + h * d, b) - cm.value(x - h * d, b)) / (2 * h)
27
+ assert abs(D1 - fd1) < 1e-6 * max(1, abs(fd1))
28
+ D1b, D2 = cm.d1_d2(x, d, b)
29
+ fd2 = (cm.d1_d2(x + h * d, d, b)[0] - cm.d1_d2(x - h * d, d, b)[0]) / (2 * h)
30
+ assert abs(D2 - fd2) < 1e-5 * max(1, abs(fd2))
31
+
32
+
33
+ def test_nominal_model_is_beta_zero():
34
+ g = soft_compile(model, n_params=N, n_bias=N)
35
+ x, d = np.full(N, 0.2), np.ones(N)
36
+ assert abs(g(x, d) - g.calibration.d1_d2(x, d, np.zeros(N))[0]) < 1e-12
37
+
38
+
39
+ def test_default_path_unchanged():
40
+ """calibrate=False must be bit-identical to the same SoftOpt without the calibrate code."""
41
+ g = soft_compile(lambda th: model(th, [0.0] * N), n_params=N)
42
+ a = SoftOpt(N, g, lr=0.05, seed=3); b = SoftOpt(N, g, lr=0.05, seed=3, calibrate=False)
43
+ x1 = x2 = np.zeros(N); rng = np.random.default_rng(4)
44
+ for _ in range(30):
45
+ gr = rng.normal(size=N)
46
+ x1, x2 = a.step(x1, gr), b.step(x2, gr)
47
+ assert np.array_equal(x1, x2)
48
+
49
+
50
+ def _run(s, mode, budget=200):
51
+ rng_b = np.random.default_rng(500 + s); btrue = rng_b.normal(0, 0.3, N); noise = np.random.default_rng(900 + s)
52
+ meas = lambda x: model(x, btrue) + 0.05 * noise.normal() # noqa: E731
53
+ g = soft_compile(model, n_params=N, n_bias=N)
54
+ kw = dict(calibrate=True, bias_scale=0.3) if mode == 'cal' else dict(slope='measured')
55
+ opt = SoftOpt(N, g, lr=0.05, seed=s * 7 + 2, **kw)
56
+ x = np.zeros(N); rng = np.random.default_rng(s * 7 + 1)
57
+ for _ in range(budget // 2):
58
+ d = rng.choice([-1.0, 1.0], N); sl = (meas(x + 0.1 * d) - meas(x - 0.1 * d)) / 0.2
59
+ x = opt.step(x, sl * d, direction=d, measured_slope=sl)
60
+ from scipy.optimize import minimize
61
+ return model(x, btrue) - minimize(lambda z: model(z, btrue), CC + btrue).fun
62
+
63
+
64
+ def test_calibration_helps_on_a_biased_model():
65
+ pytest.importorskip('scipy')
66
+ cal = np.array([_run(s, 'cal') for s in range(8)]); mea = np.array([_run(s, 'measured') for s in range(8)])
67
+ assert np.median(cal) < np.median(mea) and (cal < mea).sum() >= 6
68
+
69
+
70
+ def test_beta_free_calibration_runs():
71
+ g = soft_compile(lambda th: model(th, [0.0] * N), n_params=N)
72
+ opt = SoftOpt(N, g, lr=0.05, seed=0, calibrate=True)
73
+ x = np.zeros(N); rng = np.random.default_rng(1)
74
+ for _ in range(20):
75
+ d = rng.choice([-1.0, 1.0], N); sl = float(rng.normal())
76
+ x = opt.step(x, sl * d, direction=d, measured_slope=sl)
77
+ assert np.all(np.isfinite(x)) and opt.calibration['beta'].size == 0
78
+
79
+
80
+ def test_calibrate_requires_direction_and_scale():
81
+ g = soft_compile(model, n_params=N, n_bias=N)
82
+ with pytest.raises(ValueError):
83
+ SoftOpt(N, g, calibrate=True) # bias parameters need bias_scale
84
+ opt = SoftOpt(N, g, calibrate=True, bias_scale=0.1)
85
+ with pytest.raises(ValueError):
86
+ opt.step(np.zeros(N), np.zeros(N))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes