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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.5.2
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 and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
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) | 93.9% | 17/20 |
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 and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
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.9% | 17/20 |
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.2"
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" }
@@ -23,4 +23,4 @@ __all__ = [
23
23
  "sadd", "sneg", "ssub", "smul", "sinv", "sdiv",
24
24
  "spow", "sroot", "ssqrt", "ssin", "scos", "sexp", "slog",
25
25
  ]
26
- __version__ = "0.5.2"
26
+ __version__ = "0.5.4"
@@ -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 a finite difference of
60
- the same model evaluated with plain floats."""
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
- for attempt in range(5):
63
- theta = rng.normal(0.5, 1.0, n_params)
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 ZeroDivisionError:
68
- continue # landed on a singular point; try another
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 direction here; uninformative, try another
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
- raise RuntimeError(
88
- f"compiled derivative ({exact:.8g}) disagrees with a finite "
89
- f"difference of your model ({approx:.8g}). Your model may use an "
90
- "operation the soft algebra doesn't define, or may not be "
91
- "differentiable at this point. Pass verify=False to skip this "
92
- "check if you're confident it's correct."
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 an exact Newton
3
- correction computed from your own model.
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 derived from the exact first and
11
- second directional derivatives of your model.
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(gen_rng)
83
- delta_f = gen_rng.choice([-1.0, 1.0], size=n) if _test_magnitude_source == 'foreign_point_and_dir' else delta
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
- b_root = b ** (1.0 / n)
91
- a_root = a / (n * b ** ((n - 1) / n))
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|: the real part determines the branch; the derivative follows
202
- its sign (undefined exactly at 0, as for any absolute value)."""
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: ordered by the real component (book Definition 3.1,
208
- # "Order": if a < b then a0 < b0). This lets models branch on parameter
209
- # values normally; the compiled derivative is then valid on that branch.
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.2
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 and curvature of that model on every step, and uses them to correct the optimizer's trajectory. Validated, with real hardware-noise-model data, across:
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) | 93.9% | 17/20 |
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