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.
- {softopt-0.6.4/softopt.egg-info → softopt-0.7.0}/PKG-INFO +115 -51
- {softopt-0.6.4 → softopt-0.7.0}/README.md +112 -48
- {softopt-0.6.4 → softopt-0.7.0}/pyproject.toml +3 -3
- {softopt-0.6.4 → softopt-0.7.0}/softopt/__init__.py +7 -7
- softopt-0.7.0/softopt/calibrate.py +159 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt/compile.py +12 -1
- {softopt-0.6.4 → softopt-0.7.0}/softopt/numpy_optimizer.py +40 -1
- {softopt-0.6.4 → softopt-0.7.0/softopt.egg-info}/PKG-INFO +115 -51
- {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/SOURCES.txt +2 -0
- softopt-0.7.0/tests/test_calibrate.py +86 -0
- {softopt-0.6.4 → softopt-0.7.0}/LICENSE +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/setup.cfg +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt/soft_number.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt/torch_optimizer.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/dependency_links.txt +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/requires.txt +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/softopt.egg-info/top_level.txt +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_basic.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_causal.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_compile.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_exact_d2.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_measured_slope.py +0 -0
- {softopt-0.6.4 → softopt-0.7.0}/tests/test_soft_number.py +0 -0
- {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.
|
|
4
|
-
Summary:
|
|
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
|
-
**
|
|
29
|
+
**An optimization layer that closes the gap between your model and the real system.**
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
`model(theta) - data` and get back an *array*, rather than a single number?
|
|
49
|
+
Free, open source, and runs locally.
|
|
39
50
|
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
**
|
|
3
|
+
**An optimization layer that closes the gap between your model and the real system.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
`model(theta) - data` and get back an *array*, rather than a single number?
|
|
23
|
+
Free, open source, and runs locally.
|
|
13
24
|
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
8
|
-
description = "
|
|
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 —
|
|
3
|
-
|
|
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
|
-
|
|
13
|
-
(the
|
|
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.
|
|
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",
|
|
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.
|
|
4
|
-
Summary:
|
|
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
|
-
**
|
|
29
|
+
**An optimization layer that closes the gap between your model and the real system.**
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
`model(theta) - data` and get back an *array*, rather than a single number?
|
|
49
|
+
Free, open source, and runs locally.
|
|
39
50
|
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|