softopt 0.5.2__tar.gz → 0.5.4__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.5.2/softopt.egg-info → softopt-0.5.4}/PKG-INFO +3 -3
- {softopt-0.5.2 → softopt-0.5.4}/README.md +2 -2
- {softopt-0.5.2 → softopt-0.5.4}/pyproject.toml +1 -1
- {softopt-0.5.2 → softopt-0.5.4}/softopt/__init__.py +1 -1
- {softopt-0.5.2 → softopt-0.5.4}/softopt/compile.py +38 -24
- {softopt-0.5.2 → softopt-0.5.4}/softopt/numpy_optimizer.py +17 -6
- {softopt-0.5.2 → softopt-0.5.4}/softopt/soft_number.py +18 -7
- {softopt-0.5.2 → softopt-0.5.4/softopt.egg-info}/PKG-INFO +3 -3
- {softopt-0.5.2 → softopt-0.5.4}/tests/test_compile.py +13 -0
- {softopt-0.5.2 → softopt-0.5.4}/tests/test_soft_number.py +19 -0
- {softopt-0.5.2 → softopt-0.5.4}/LICENSE +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/setup.cfg +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/softopt.egg-info/SOURCES.txt +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/softopt.egg-info/dependency_links.txt +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/softopt.egg-info/requires.txt +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/softopt.egg-info/top_level.txt +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/tests/test_basic.py +0 -0
- {softopt-0.5.2 → softopt-0.5.4}/tests/test_causal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: softopt
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
5
|
Author: Ido Angel
|
|
6
6
|
License: MIT
|
|
@@ -30,7 +30,7 @@ Each step is an update followed by an exact Newton correction computed from your
|
|
|
30
30
|
|
|
31
31
|
## Where it helps
|
|
32
32
|
|
|
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
|
|
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:
|
|
34
34
|
|
|
35
35
|
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
36
36
|
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
@@ -114,7 +114,7 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
114
114
|
|
|
115
115
|
| Domain | Improvement | Win rate |
|
|
116
116
|
|---|---|---|
|
|
117
|
-
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) |
|
|
117
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 91.6% | 16/20 |
|
|
118
118
|
|
|
119
119
|
**Finance**
|
|
120
120
|
|
|
@@ -6,7 +6,7 @@ Each step is an update followed by an exact Newton correction computed from your
|
|
|
6
6
|
|
|
7
7
|
## Where it helps
|
|
8
8
|
|
|
9
|
-
If your problem has a **known computation graph** — a quantum circuit, a physical simulator, a projection or measurement model, anything you can write down exactly, even if the *measurements* of it are noisy — SoftOpt computes an exact directional derivative
|
|
9
|
+
If your problem has a **known computation graph** — a quantum circuit, a physical simulator, a projection or measurement model, anything you can write down exactly, even if the *measurements* of it are noisy — SoftOpt computes an exact directional derivative 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:
|
|
10
10
|
|
|
11
11
|
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
12
12
|
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
@@ -90,7 +90,7 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
90
90
|
|
|
91
91
|
| Domain | Improvement | Win rate |
|
|
92
92
|
|---|---|---|
|
|
93
|
-
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) |
|
|
93
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 91.6% | 16/20 |
|
|
94
94
|
|
|
95
95
|
**Finance**
|
|
96
96
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "softopt"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.4"
|
|
8
8
|
description = "A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -55,17 +55,30 @@ def soft_compile(model_fn, n_params=None, verify=True, verify_tol=1e-5):
|
|
|
55
55
|
return g_delta
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def _verify(model_fn, g_delta, n_params, tol):
|
|
59
|
-
"""Compare the exact compiled derivative against
|
|
60
|
-
|
|
58
|
+
def _verify(model_fn, g_delta, n_params, tol, n_points=6):
|
|
59
|
+
"""Compare the exact compiled derivative against finite differences of the
|
|
60
|
+
same model, at several scattered points rather than one.
|
|
61
|
+
|
|
62
|
+
A single passing point proves very little: a model that branches can be
|
|
63
|
+
correct on one side and silently wrong on the other, and an operation that
|
|
64
|
+
discards the traced derivative (float(), np.array on the parameters) may
|
|
65
|
+
only sit inside one branch. Checking a spread of points catches that."""
|
|
61
66
|
rng = np.random.default_rng(0)
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
checked = 0
|
|
68
|
+
attempts = 0
|
|
69
|
+
while checked < n_points and attempts < n_points * 6:
|
|
70
|
+
attempts += 1
|
|
71
|
+
# spread the test points over several scales and both signs, so that
|
|
72
|
+
# branch conditions like `if x > 0` are exercised on both sides
|
|
73
|
+
spread = rng.choice([0.3, 1.0, 3.0])
|
|
74
|
+
theta = rng.normal(0.0, spread, n_params)
|
|
64
75
|
direction = rng.normal(0, 1, n_params)
|
|
65
76
|
try:
|
|
66
77
|
exact = g_delta(theta, direction)
|
|
67
|
-
except
|
|
68
|
-
|
|
78
|
+
except TypeError:
|
|
79
|
+
raise # the model dropped the traced parameters -- a real fault
|
|
80
|
+
except (ZeroDivisionError, ValueError):
|
|
81
|
+
continue # landed outside the model's domain; try elsewhere
|
|
69
82
|
h = 1e-6
|
|
70
83
|
try:
|
|
71
84
|
f_plus = float(model_fn(list(theta + h * direction)))
|
|
@@ -79,23 +92,24 @@ def _verify(model_fn, g_delta, n_params, tol):
|
|
|
79
92
|
approx = (f_plus - f_minus) / (2 * h)
|
|
80
93
|
|
|
81
94
|
scale = max(abs(approx), abs(exact), 1.0)
|
|
82
|
-
if abs(exact - approx) / scale < tol:
|
|
83
|
-
return # verified
|
|
84
95
|
if abs(approx) < 1e-12 and abs(exact) < 1e-12:
|
|
85
|
-
continue # flat
|
|
96
|
+
continue # flat here; uninformative, doesn't count as a check
|
|
97
|
+
checked += 1
|
|
98
|
+
if abs(exact - approx) / scale >= tol:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
f"at theta={np.array2string(theta, precision=4)} the compiled "
|
|
101
|
+
f"derivative ({exact:.8g}) disagrees with a finite difference of "
|
|
102
|
+
f"your model ({approx:.8g}).\n\nIf your model branches, one branch "
|
|
103
|
+
"may use an operation that discards the traced derivative -- "
|
|
104
|
+
"float(), np.array() on the parameter list, or a call into a "
|
|
105
|
+
"library that only accepts plain numbers. Pass verify=False to "
|
|
106
|
+
"skip this check if you are confident the model is correct."
|
|
107
|
+
)
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
109
|
+
if checked == 0:
|
|
110
|
+
import warnings
|
|
111
|
+
warnings.warn(
|
|
112
|
+
"soft_compile could not verify the model: every test point was "
|
|
113
|
+
"either singular or flat. The compiled function is returned unchecked.",
|
|
114
|
+
RuntimeWarning,
|
|
93
115
|
)
|
|
94
|
-
# every attempt landed somewhere uninformative -- don't fail, but don't
|
|
95
|
-
# claim verification either
|
|
96
|
-
import warnings
|
|
97
|
-
warnings.warn(
|
|
98
|
-
"soft_compile could not verify the model: every test point was "
|
|
99
|
-
"either singular or flat. The compiled function is returned unchecked.",
|
|
100
|
-
RuntimeWarning,
|
|
101
|
-
)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
|
-
SoftOpt: each step() call performs an update followed by
|
|
3
|
-
|
|
2
|
+
SoftOpt: each step() call performs an update followed by a Newton correction
|
|
3
|
+
computed from your own model.
|
|
4
4
|
|
|
5
5
|
Requirement: you supply g_delta(theta, delta) -> the exact directional
|
|
6
6
|
derivative of your true objective along delta, computed from your own
|
|
7
7
|
known model (circuit, projection, physical law). `soft_compile` generates
|
|
8
8
|
this for you from a model written in ordinary Python.
|
|
9
9
|
|
|
10
|
-
The correction is a bounded Newton step
|
|
11
|
-
|
|
10
|
+
The correction is a bounded Newton step. The first directional derivative is
|
|
11
|
+
exact, propagated through your model by soft-number arithmetic. The second is
|
|
12
|
+
a central finite difference OF that exact derivative -- an approximation, with
|
|
13
|
+
the usual step-size trade-off, controlled by h_fd.
|
|
12
14
|
"""
|
|
13
15
|
import numpy as np
|
|
14
16
|
|
|
@@ -29,6 +31,7 @@ class SoftOpt:
|
|
|
29
31
|
self.v = None
|
|
30
32
|
self.t = 0
|
|
31
33
|
self.rng = np.random.default_rng(seed)
|
|
34
|
+
self._ablation_rng = None
|
|
32
35
|
self._test_frozen_constant = _test_frozen_constant
|
|
33
36
|
|
|
34
37
|
def step(self, theta, grad_estimate, _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
|
|
@@ -69,6 +72,13 @@ class SoftOpt:
|
|
|
69
72
|
n = theta.shape[0]
|
|
70
73
|
gen_rng = _test_rng if _test_rng is not None else self.rng
|
|
71
74
|
delta = gen_rng.choice([-1.0, 1.0], size=n)
|
|
75
|
+
|
|
76
|
+
# The ablation arms must not disturb the probe-direction stream, or
|
|
77
|
+
# a corrupted arm would also change every direction that follows and
|
|
78
|
+
# the comparison would no longer be like-for-like.
|
|
79
|
+
if self._ablation_rng is None:
|
|
80
|
+
self._ablation_rng = np.random.default_rng(
|
|
81
|
+
self.rng.integers(0, 2**63 - 1))
|
|
72
82
|
D1 = self.g_delta(theta_after_adam, delta)
|
|
73
83
|
gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
|
|
74
84
|
gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
|
|
@@ -79,8 +89,9 @@ class SoftOpt:
|
|
|
79
89
|
elif _test_magnitude_source == 'frozen':
|
|
80
90
|
D2_used = self._test_frozen_constant
|
|
81
91
|
elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
|
|
82
|
-
theta_f = _test_foreign_sampler(
|
|
83
|
-
delta_f =
|
|
92
|
+
theta_f = _test_foreign_sampler(self._ablation_rng)
|
|
93
|
+
delta_f = (self._ablation_rng.choice([-1.0, 1.0], size=n)
|
|
94
|
+
if _test_magnitude_source == 'foreign_point_and_dir' else delta)
|
|
84
95
|
gp_f = self.g_delta(theta_f + self.h_fd * delta_f, delta_f)
|
|
85
96
|
gm_f = self.g_delta(theta_f - self.h_fd * delta_f, delta_f)
|
|
86
97
|
D2_used = abs((gp_f - gm_f) / (2 * self.h_fd))
|
|
@@ -87,8 +87,14 @@ def sroot(x, n):
|
|
|
87
87
|
if b < 0 and n % 2 == 0:
|
|
88
88
|
raise ValueError(f"even root (n={n}) of a soft number with b<0 "
|
|
89
89
|
"is not real-valued (Lemma 3.4)")
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
if b < 0:
|
|
91
|
+
# Odd root of a negative real component: Python's ** returns a complex
|
|
92
|
+
# principal root here, so take the real root of |b| and restore the sign.
|
|
93
|
+
b_root = -((-b) ** (1.0 / n))
|
|
94
|
+
else:
|
|
95
|
+
b_root = b ** (1.0 / n)
|
|
96
|
+
# d/db b^(1/n) = b^(1/n) / (n*b), valid for either sign
|
|
97
|
+
a_root = a * b_root / (n * b)
|
|
92
98
|
return (a_root, b_root)
|
|
93
99
|
|
|
94
100
|
|
|
@@ -198,15 +204,20 @@ class SoftNumber:
|
|
|
198
204
|
return SoftNumber(self.a / (self.b * np.log(10.0)), np.log10(self.b))
|
|
199
205
|
|
|
200
206
|
def __abs__(self):
|
|
201
|
-
"""|x
|
|
202
|
-
|
|
207
|
+
"""|x|. The real component picks the branch. At exactly zero the
|
|
208
|
+
derivative of |x| does not exist, so the zero-axis component is
|
|
209
|
+
reported as nan rather than silently taking one side."""
|
|
203
210
|
if self.b < 0:
|
|
204
211
|
return -self
|
|
212
|
+
if self.b == 0:
|
|
213
|
+
return SoftNumber(float("nan") if self.a != 0 else 0.0, 0.0)
|
|
205
214
|
return SoftNumber(self.a, self.b)
|
|
206
215
|
|
|
207
|
-
# --- comparisons
|
|
208
|
-
#
|
|
209
|
-
#
|
|
216
|
+
# --- comparisons. These order by the REAL component only, which is what
|
|
217
|
+
# lets a model branch on a parameter value; the compiled derivative is then
|
|
218
|
+
# valid on whichever branch the parameters fall in. Note this is a practical
|
|
219
|
+
# ordering for tracing, not the book's Definition 3.1 ordering of the zero
|
|
220
|
+
# axis itself (a0 < b0 when a < b), which concerns the other component.
|
|
210
221
|
def __lt__(self, other):
|
|
211
222
|
return self.b < self._coerce(other).b
|
|
212
223
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: softopt
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: A standalone optimizer for problems with a known computation graph, built on Klein–Maimon soft-number calculus
|
|
5
5
|
Author: Ido Angel
|
|
6
6
|
License: MIT
|
|
@@ -30,7 +30,7 @@ Each step is an update followed by an exact Newton correction computed from your
|
|
|
30
30
|
|
|
31
31
|
## Where it helps
|
|
32
32
|
|
|
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
|
|
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:
|
|
34
34
|
|
|
35
35
|
- **Quantum chemistry (VQE)** — H₂, H₄, BeH₂, HeH⁺, and larger multireference molecules
|
|
36
36
|
- **Quantum control (GRAPE)** — the single cleanest result across every domain tested
|
|
@@ -114,7 +114,7 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
|
|
|
114
114
|
|
|
115
115
|
| Domain | Improvement | Win rate |
|
|
116
116
|
|---|---|---|
|
|
117
|
-
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) |
|
|
117
|
+
| Camera calibration (bundle adjustment, realistic pixel + outlier noise) | 91.6% | 16/20 |
|
|
118
118
|
|
|
119
119
|
**Finance**
|
|
120
120
|
|
|
@@ -145,3 +145,16 @@ def test_model_that_casts_to_float_is_caught():
|
|
|
145
145
|
return float(v @ v)
|
|
146
146
|
with pytest.raises(TypeError, match="doesn't depend on the traced parameters"):
|
|
147
147
|
soft_compile(model, n_params=2)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_a_branch_that_drops_the_derivative_is_caught():
|
|
151
|
+
"""One passing point is not enough: a model can be correct on one branch
|
|
152
|
+
and silently wrong on the other."""
|
|
153
|
+
def branching(p):
|
|
154
|
+
x, y = p
|
|
155
|
+
if x > 0:
|
|
156
|
+
return x * x + 2.0 * y
|
|
157
|
+
return x * x + float(y) * 2.0 # float() discards y's derivative
|
|
158
|
+
|
|
159
|
+
with pytest.raises(RuntimeError, match="disagrees with a finite difference"):
|
|
160
|
+
soft_compile(branching, n_params=2)
|
|
@@ -121,3 +121,22 @@ def test_class_matches_tuple_based_internals_exactly(xyz):
|
|
|
121
121
|
assert np.allclose((x + y).as_tuple(), sadd(xt, yt))
|
|
122
122
|
assert np.allclose((x * y).as_tuple(), smul(xt, yt))
|
|
123
123
|
assert np.allclose(x.inverse().as_tuple(), sinv(xt))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_odd_root_of_a_negative_real_component_is_real():
|
|
127
|
+
"""Python's ** returns the complex principal root for (-8)**(1/3);
|
|
128
|
+
the real root is the one this algebra means."""
|
|
129
|
+
from softopt import sroot, spow
|
|
130
|
+
r = sroot((1.0, -8.0), 3)
|
|
131
|
+
assert not isinstance(r[0], complex) and not isinstance(r[1], complex)
|
|
132
|
+
assert np.isclose(r[1], -2.0)
|
|
133
|
+
assert np.isclose(r[0], 1.0 / 12.0)
|
|
134
|
+
back = spow(r, 3)
|
|
135
|
+
assert np.isclose(back[0], 1.0) and np.isclose(back[1], -8.0)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_abs_reports_an_undefined_derivative_at_the_kink():
|
|
139
|
+
"""|x| has no derivative at zero; report that rather than picking a side."""
|
|
140
|
+
assert np.isnan(abs(SoftNumber(1.0, 0.0)).a)
|
|
141
|
+
assert abs(SoftNumber(1.0, -2.0)) == SoftNumber(-1.0, 2.0)
|
|
142
|
+
assert abs(SoftNumber(1.0, 2.0)) == SoftNumber(1.0, 2.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|