softopt 0.3.0__tar.gz → 0.5.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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.3.0
3
+ Version: 0.5.0
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
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,quantum-computing,vqe,qaoa,pytorch
10
+ Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,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
@@ -18,11 +18,8 @@ Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: numpy>=1.20
21
- Provides-Extra: torch
22
- Requires-Dist: torch>=2.0; extra == "torch"
23
21
  Provides-Extra: dev
24
22
  Requires-Dist: pytest>=7.0; extra == "dev"
25
- Requires-Dist: torch>=2.0; extra == "dev"
26
23
  Dynamic: license-file
27
24
 
28
25
  # SoftOpt
@@ -49,14 +46,11 @@ Full reinforcement learning (or anything else with a continuously **moving targe
49
46
  ## Installation
50
47
 
51
48
  ```bash
52
- pip install softopt # numpy version only
53
- pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
49
+ pip install softopt
54
50
  ```
55
51
 
56
52
  ## Quick start
57
53
 
58
- ### NumPy
59
-
60
54
  ```python
61
55
  import numpy as np
62
56
  from softopt import SoftOpt, soft_compile
@@ -83,28 +77,6 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
83
77
 
84
78
  If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
85
79
 
86
- ### PyTorch
87
-
88
- ```python
89
- from softopt import SoftOptTorch
90
-
91
- opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
92
-
93
- for batch in data:
94
- loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
95
- ```
96
-
97
- `loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
98
-
99
- ## Two correction modes
100
-
101
- SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
102
-
103
- - **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
104
- - **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
105
-
106
- **How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
107
-
108
80
  ## Validated results
109
81
 
110
82
  All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
@@ -22,14 +22,11 @@ Full reinforcement learning (or anything else with a continuously **moving targe
22
22
  ## Installation
23
23
 
24
24
  ```bash
25
- pip install softopt # numpy version only
26
- pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
25
+ pip install softopt
27
26
  ```
28
27
 
29
28
  ## Quick start
30
29
 
31
- ### NumPy
32
-
33
30
  ```python
34
31
  import numpy as np
35
32
  from softopt import SoftOpt, soft_compile
@@ -56,28 +53,6 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
56
53
 
57
54
  If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
58
55
 
59
- ### PyTorch
60
-
61
- ```python
62
- from softopt import SoftOptTorch
63
-
64
- opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
65
-
66
- for batch in data:
67
- loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
68
- ```
69
-
70
- `loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
71
-
72
- ## Two correction modes
73
-
74
- SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
75
-
76
- - **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
77
- - **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
78
-
79
- **How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
80
-
81
56
  ## Validated results
82
57
 
83
58
  All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "softopt"
7
- version = "0.3.0"
7
+ version = "0.5.0"
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" }
@@ -12,7 +12,7 @@ requires-python = ">=3.9"
12
12
  authors = [
13
13
  { name = "Ido Angel" }
14
14
  ]
15
- keywords = ["optimizer", "optimization", "soft-logic", "soft-numbers", "quantum-computing", "vqe", "qaoa", "pytorch"]
15
+ keywords = ["optimizer", "optimization", "soft-logic", "soft-numbers", "uncertainty", "sensitivity", "quantum-computing", "vqe"]
16
16
  classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Intended Audience :: Science/Research",
@@ -26,8 +26,7 @@ dependencies = [
26
26
  ]
27
27
 
28
28
  [project.optional-dependencies]
29
- torch = ["torch>=2.0"]
30
- dev = ["pytest>=7.0", "torch>=2.0"]
29
+ dev = ["pytest>=7.0"]
31
30
 
32
31
  [project.urls]
33
32
  Homepage = "https://github.com/mobiuai/softopt"
@@ -23,11 +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.3.0"
27
-
28
- try:
29
- from .torch_optimizer import SoftOpt as SoftOptTorch
30
- __all__.append("SoftOptTorch")
31
- except ImportError:
32
- # torch is an optional dependency (pip install softopt[torch])
33
- pass
26
+ __version__ = "0.5.0"
@@ -0,0 +1,98 @@
1
+ """
2
+ SoftOpt: a COMPLETE, standalone optimizer -- not a wrapper around Adam.
3
+ Exactly one step() call does both the Adam-style base update AND the
4
+ validated Newton-style correction. You don't bring your own optimizer;
5
+ this IS the optimizer, in the same sense that torch.optim.Adam IS an
6
+ optimizer (internally momentum + RMS-scaling), not a wrapper around SGD.
7
+
8
+ Requirement: you supply g_delta(theta, delta) -> the exact directional
9
+ derivative of your true objective along delta, computed from your own
10
+ known model (circuit, projection, physical law). `soft_compile` generates
11
+ this for you from a model written in ordinary Python.
12
+
13
+ The correction is a bounded Newton step derived from the exact first and
14
+ second directional derivatives of your model.
15
+ """
16
+ import numpy as np
17
+
18
+
19
+ class SoftOpt:
20
+ def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
21
+ newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
22
+ h_fd=1e-4, seed=None, _test_frozen_constant=1.0):
23
+ self.g_delta = g_delta
24
+ self.lr = lr
25
+ self.b1, self.b2 = betas
26
+ self.eps = eps
27
+ self.newton_lo = newton_lo
28
+ self.newton_hi = newton_hi
29
+ self.eta_fallback = eta_fallback
30
+ self.h_fd = h_fd
31
+ self.m = None
32
+ self.v = None
33
+ self.t = 0
34
+ self.rng = np.random.default_rng(seed)
35
+ self._test_frozen_constant = _test_frozen_constant
36
+
37
+ def step(self, theta, grad_estimate, _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
38
+ """One complete optimizer step: Adam base update + Newton-style
39
+ correction from the known model, in a single call.
40
+ theta: current parameters (np.ndarray)
41
+ grad_estimate: a gradient estimate of the loss w.r.t. theta (e.g.
42
+ from SPSA or backprop) -- exactly what you'd normally hand
43
+ to Adam.
44
+ Returns: theta for the next step.
45
+
46
+ The _test_* arguments are TEST-ONLY hooks used exclusively by the
47
+ causal-validation ablation harness to deliberately corrupt the
48
+ correction (feed a frozen constant or a foreign point's curvature
49
+ instead of the genuine one) so that "does real content matter"
50
+ can be checked against the EXACT SAME code path real usage takes.
51
+ Normal usage never sets these; leaving them unset gives the
52
+ genuine, real behavior.
53
+ """
54
+ theta = np.asarray(theta, dtype=float)
55
+ grad_estimate = np.asarray(grad_estimate, dtype=float)
56
+
57
+ # --- Adam base update ---
58
+ self.t += 1
59
+ if self.m is None:
60
+ self.m = np.zeros_like(theta)
61
+ self.v = np.zeros_like(theta)
62
+ self.m = self.b1 * self.m + (1 - self.b1) * grad_estimate
63
+ self.v = self.b2 * self.v + (1 - self.b2) * grad_estimate ** 2
64
+ mh = self.m / (1 - self.b1 ** self.t)
65
+ vh = self.v / (1 - self.b2 ** self.t)
66
+ theta_after_adam = theta - self.lr * mh / (np.sqrt(vh) + self.eps)
67
+
68
+ if _test_magnitude_source == 'plain':
69
+ return theta_after_adam
70
+
71
+ # --- correction from the known, exact model ---
72
+ n = theta.shape[0]
73
+ gen_rng = _test_rng if _test_rng is not None else self.rng
74
+ delta = gen_rng.choice([-1.0, 1.0], size=n)
75
+ D1 = self.g_delta(theta_after_adam, delta)
76
+ gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
77
+ gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
78
+ D2_real = (gp - gm) / (2 * self.h_fd)
79
+
80
+ if _test_magnitude_source in (None, 'real'):
81
+ D2_used = D2_real
82
+ elif _test_magnitude_source == 'frozen':
83
+ D2_used = self._test_frozen_constant
84
+ elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
85
+ theta_f = _test_foreign_sampler(gen_rng)
86
+ delta_f = gen_rng.choice([-1.0, 1.0], size=n) if _test_magnitude_source == 'foreign_point_and_dir' else delta
87
+ gp_f = self.g_delta(theta_f + self.h_fd * delta_f, delta_f)
88
+ gm_f = self.g_delta(theta_f - self.h_fd * delta_f, delta_f)
89
+ D2_used = abs((gp_f - gm_f) / (2 * self.h_fd))
90
+ else:
91
+ raise ValueError(_test_magnitude_source)
92
+
93
+ if D2_real > 1e-8 and D2_used > 1e-9:
94
+ t_star = float(np.clip(-D1 / D2_used, self.newton_lo, self.newton_hi))
95
+ else:
96
+ t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
97
+
98
+ return theta_after_adam + t_star * delta
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.3.0
3
+ Version: 0.5.0
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
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,quantum-computing,vqe,qaoa,pytorch
10
+ Keywords: optimizer,optimization,soft-logic,soft-numbers,uncertainty,sensitivity,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
@@ -18,11 +18,8 @@ Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: numpy>=1.20
21
- Provides-Extra: torch
22
- Requires-Dist: torch>=2.0; extra == "torch"
23
21
  Provides-Extra: dev
24
22
  Requires-Dist: pytest>=7.0; extra == "dev"
25
- Requires-Dist: torch>=2.0; extra == "dev"
26
23
  Dynamic: license-file
27
24
 
28
25
  # SoftOpt
@@ -49,14 +46,11 @@ Full reinforcement learning (or anything else with a continuously **moving targe
49
46
  ## Installation
50
47
 
51
48
  ```bash
52
- pip install softopt # numpy version only
53
- pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
49
+ pip install softopt
54
50
  ```
55
51
 
56
52
  ## Quick start
57
53
 
58
- ### NumPy
59
-
60
54
  ```python
61
55
  import numpy as np
62
56
  from softopt import SoftOpt, soft_compile
@@ -83,28 +77,6 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
83
77
 
84
78
  If you'd rather write the derivative function yourself — because your model lives in a simulator SoftOpt can't trace, or because you want the speed of a hand-tuned implementation — pass any `g_delta(theta, delta)` that returns the exact directional derivative, and skip `soft_compile` entirely.
85
79
 
86
- ### PyTorch
87
-
88
- ```python
89
- from softopt import SoftOptTorch
90
-
91
- opt = SoftOptTorch(model.parameters(), model, loss_fn, lr=1e-3)
92
-
93
- for batch in data:
94
- loss = opt.step(batch) # one call: backward + Adam update + soft-number correction
95
- ```
96
-
97
- `loss_fn(model, batch)` must be an exactly re-evaluatable, differentiable function of the model's own parameters — a full-batch loss, a physics simulator, a known projection model. SoftOpt uses PyTorch's own forward-mode autodiff (`torch.func.jvp`) to get the same exact directional derivative and curvature the NumPy version computes by hand.
98
-
99
- ## Two correction modes
100
-
101
- SoftOpt ships with two ways of turning the computed derivative/curvature into a parameter update:
102
-
103
- - **`newton`** (default) — a bounded Newton step `t* = clip(-D1/D2, bounds)`. Best when the curvature (D2) is consistently one-signed along random directions — true for essentially every gradient-based physics/circuit optimization problem we tested (VQE, GRAPE, camera calibration, portfolio, ...).
104
- - **`mobius`** — a bounded, sign-safe step derived from the book's own Möbius map (Ch. 5.3), for problems whose curvature is *not* reliably one-signed. Pass `mode="mobius"` to `SoftOpt`/`SoftOptTorch` if `newton` underperforms plain Adam on your problem — that pattern is itself informative about your landscape's curvature.
105
-
106
- **How do I know which one to use?** Right now, empirically: run a short comparison against plain Adam with `mode="newton"` first; if it clearly loses, try `mode="mobius"`. There is also an experimental `mode="auto"` that samples curvature sign near your starting point and picks for you — we tested it honestly and it is **not yet reliable** (on GRAPE, a domain we know needs `newton` with high confidence, it only picked correctly 40% of the time across random starting points). It's included so you can inspect `opt.detected_mode` and help us characterize when it works, but don't depend on it yet.
107
-
108
80
  ## Validated results
109
81
 
110
82
  All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic finite-sample/measurement noise for the non-quantum domains, with `torch.optim.Adam` as the baseline. Improvement is the reduction in gap to the known optimum (or, for Portfolio, the reduction in loss).
@@ -5,7 +5,6 @@ softopt/__init__.py
5
5
  softopt/compile.py
6
6
  softopt/numpy_optimizer.py
7
7
  softopt/soft_number.py
8
- softopt/torch_optimizer.py
9
8
  softopt.egg-info/PKG-INFO
10
9
  softopt.egg-info/SOURCES.txt
11
10
  softopt.egg-info/dependency_links.txt
@@ -2,7 +2,3 @@ numpy>=1.20
2
2
 
3
3
  [dev]
4
4
  pytest>=7.0
5
- torch>=2.0
6
-
7
- [torch]
8
- torch>=2.0
@@ -14,7 +14,7 @@ def quadratic_g_delta(theta, delta):
14
14
  return float(2 * theta @ delta)
15
15
 
16
16
 
17
- def test_newton_mode_converges_on_convex_quadratic():
17
+ def test_converges_on_convex_quadratic():
18
18
  opt = SoftOpt(5, quadratic_g_delta, lr=0.1, seed=0)
19
19
  theta = np.ones(5)
20
20
  for _ in range(200):
@@ -23,20 +23,6 @@ def test_newton_mode_converges_on_convex_quadratic():
23
23
  assert np.allclose(theta, 0.0, atol=1e-2)
24
24
 
25
25
 
26
- def test_mobius_mode_runs_without_error():
27
- opt = SoftOpt(5, quadratic_g_delta, lr=0.1, mode="mobius", seed=0)
28
- theta = np.ones(5)
29
- for _ in range(50):
30
- grad = 2 * theta
31
- theta = opt.step(theta, grad)
32
- assert np.all(np.isfinite(theta))
33
-
34
-
35
- def test_invalid_mode_raises():
36
- with pytest.raises(ValueError):
37
- SoftOpt(5, quadratic_g_delta, mode="not_a_real_mode")
38
-
39
-
40
26
  def test_step_is_deterministic_given_seed():
41
27
  opt1 = SoftOpt(3, quadratic_g_delta, lr=0.05, seed=42)
42
28
  opt2 = SoftOpt(3, quadratic_g_delta, lr=0.05, seed=42)
@@ -54,11 +40,3 @@ def test_plain_arm_skips_correction():
54
40
  theta = np.array([1.0, 1.0, 1.0])
55
41
  result = opt.step(theta, 2 * theta, _test_magnitude_source="plain")
56
42
  assert np.all(np.isfinite(result))
57
-
58
-
59
- def test_torch_optimizer_importable_if_torch_present():
60
- try:
61
- import torch # noqa: F401
62
- except ImportError:
63
- pytest.skip("torch not installed")
64
- from softopt import SoftOptTorch # noqa: F401
@@ -1,168 +0,0 @@
1
- """
2
- SoftOpt: a COMPLETE, standalone optimizer -- not a wrapper around Adam.
3
- Exactly one step() call does both the Adam-style base update AND the
4
- validated Newton-style correction. You don't bring your own optimizer;
5
- this IS the optimizer, in the same sense that torch.optim.Adam IS an
6
- optimizer (internally momentum + RMS-scaling), not a wrapper around SGD.
7
-
8
- Requirement (the "known computation graph" criterion, validated across
9
- nine domains today): you must supply g_delta(theta, delta) -> the exact
10
- directional derivative of your TRUE, differentiable objective along
11
- delta, computed via Klein-Maimon soft-number propagation through your
12
- own known model (circuit, projection, force field, ...). If you cannot
13
- write g_delta without sampling the world, SoftOpt is not the right tool
14
- -- use plain Adam/SGD instead.
15
-
16
- Three correction modes (see README for details):
17
- - mode="newton" (default): bounded Newton step, best for one-signed
18
- curvature (most physics/circuit problems).
19
- - mode="mobius": bounded, sign-safe Mobius-map step (book Ch.5.3), for
20
- landscapes whose curvature isn't reliably one-signed (e.g. QAOA).
21
- - mode="auto" (EXPERIMENTAL, not yet reliable -- see README): attempts
22
- to diagnose which of the above fits your problem by sampling D2's
23
- sign near the starting point. Tested against GRAPE (a domain that
24
- needs "newton" with high confidence) it only picked "newton" 40% of
25
- the time -- a single starting region's local curvature sign is not
26
- a reliable predictor of the right mode for the whole optimization.
27
- Shipped anyway, clearly labeled, so you can inspect `opt.detected_mode`
28
- and help characterize when it does/doesn't work, but do not rely on
29
- it yet -- the empirically-validated way to choose is still: try both
30
- modes on a short run and keep whichever beats plain Adam.
31
- """
32
- import numpy as np
33
-
34
-
35
- def _mobius_B(x, y):
36
- """Book Ch.5.3's Möbius map, bounded to [-1,1] -- a sign-safe
37
- alternative to raw Newton division for landscapes whose curvature
38
- isn't reliably one-signed (e.g. QAOA, empirically)."""
39
- denom = abs(x) + abs(y)
40
- if denom < 1e-12:
41
- return 0.0
42
- sgn = 1.0 if x >= 0 else -1.0
43
- return y * sgn / denom
44
-
45
-
46
- class SoftOpt:
47
- def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
48
- mode="newton", auto_detect_steps=30, auto_detect_threshold=0.8,
49
- newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
50
- eta_mobius=0.3, h_fd=1e-4, seed=None,
51
- _test_frozen_constant=1.0):
52
- self.g_delta = g_delta
53
- self.lr = lr
54
- self.b1, self.b2 = betas
55
- self.eps = eps
56
- if mode not in ("newton", "mobius", "auto"):
57
- raise ValueError("mode must be 'newton', 'mobius', or 'auto'")
58
- self.mode = mode
59
- self.auto_detect_steps = auto_detect_steps
60
- self.auto_detect_threshold = auto_detect_threshold
61
- self._auto_pending = (mode == "auto")
62
- self.detected_mode = None if mode == "auto" else mode
63
- self.newton_lo = newton_lo
64
- self.newton_hi = newton_hi
65
- self.eta_fallback = eta_fallback
66
- self.eta_mobius = eta_mobius
67
- self.h_fd = h_fd
68
- self.m = None
69
- self.v = None
70
- self.t = 0
71
- self.rng = np.random.default_rng(seed)
72
- self._test_frozen_constant = _test_frozen_constant
73
-
74
- def _diagnose_mode(self, theta0):
75
- """Run once, before any optimization step: sample D2's sign
76
- across several nearby points (not just theta0 itself -- a single
77
- point's local curvature sign is not always representative) and
78
- several random directions per point, matching the methodology
79
- that correctly distinguished GRAPE/VQE's one-signed curvature
80
- from QAOA's sign-indefinite curvature."""
81
- n = theta0.shape[0]
82
- n_points = 5
83
- dirs_per_point = max(1, self.auto_detect_steps // n_points)
84
- signs = []
85
- for p in range(n_points):
86
- point = theta0 if p == 0 else theta0 + self.rng.normal(scale=0.1, size=n)
87
- for _ in range(dirs_per_point):
88
- delta = self.rng.choice([-1.0, 1.0], size=n)
89
- gp = self.g_delta(point + self.h_fd * delta, delta)
90
- gm = self.g_delta(point - self.h_fd * delta, delta)
91
- D2 = (gp - gm) / (2 * self.h_fd)
92
- signs.append(1.0 if D2 >= 0 else -1.0)
93
- signs = np.array(signs)
94
- agreement = max(np.mean(signs > 0), np.mean(signs < 0))
95
- self.detected_mode = "newton" if agreement >= self.auto_detect_threshold else "mobius"
96
- self.mode = self.detected_mode
97
-
98
- def step(self, theta, grad_estimate, _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
99
- """One complete optimizer step: Adam base update + Newton-style
100
- correction from the known model, in a single call.
101
- theta: current parameters (np.ndarray)
102
- grad_estimate: a gradient estimate of the loss w.r.t. theta (e.g.
103
- from SPSA or backprop) -- exactly what you'd normally hand
104
- to Adam.
105
- Returns: theta for the next step.
106
-
107
- The _test_* arguments are TEST-ONLY hooks used exclusively by the
108
- causal-validation ablation harness to deliberately corrupt the
109
- correction (feed a frozen constant or a foreign point's curvature
110
- instead of the genuine one) so that "does real content matter"
111
- can be checked against the EXACT SAME code path real usage takes.
112
- Normal usage never sets these; leaving them unset gives the
113
- genuine, real behavior.
114
- """
115
- theta = np.asarray(theta, dtype=float)
116
- grad_estimate = np.asarray(grad_estimate, dtype=float)
117
-
118
- if self._auto_pending:
119
- self._diagnose_mode(theta)
120
- self._auto_pending = False
121
-
122
- # --- Adam base update ---
123
- self.t += 1
124
- if self.m is None:
125
- self.m = np.zeros_like(theta)
126
- self.v = np.zeros_like(theta)
127
- self.m = self.b1 * self.m + (1 - self.b1) * grad_estimate
128
- self.v = self.b2 * self.v + (1 - self.b2) * grad_estimate ** 2
129
- mh = self.m / (1 - self.b1 ** self.t)
130
- vh = self.v / (1 - self.b2 ** self.t)
131
- theta_after_adam = theta - self.lr * mh / (np.sqrt(vh) + self.eps)
132
-
133
- if _test_magnitude_source == 'plain':
134
- return theta_after_adam
135
-
136
- # --- correction from the known, exact model ---
137
- n = theta.shape[0]
138
- gen_rng = _test_rng if _test_rng is not None else self.rng
139
- delta = gen_rng.choice([-1.0, 1.0], size=n)
140
- D1 = self.g_delta(theta_after_adam, delta)
141
- gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
142
- gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
143
- D2_real = (gp - gm) / (2 * self.h_fd)
144
-
145
- if _test_magnitude_source in (None, 'real'):
146
- D2_used = D2_real
147
- elif _test_magnitude_source == 'frozen':
148
- D2_used = self._test_frozen_constant
149
- elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
150
- theta_f = _test_foreign_sampler(gen_rng)
151
- delta_f = gen_rng.choice([-1.0, 1.0], size=n) if _test_magnitude_source == 'foreign_point_and_dir' else delta
152
- gp_f = self.g_delta(theta_f + self.h_fd * delta_f, delta_f)
153
- gm_f = self.g_delta(theta_f - self.h_fd * delta_f, delta_f)
154
- D2_used = abs((gp_f - gm_f) / (2 * self.h_fd))
155
- else:
156
- raise ValueError(_test_magnitude_source)
157
-
158
- if self.mode == "mobius":
159
- B = _mobius_B(D1, D2_used)
160
- t_star = self.eta_mobius * B
161
- else:
162
- activate = D2_real > 1e-8
163
- if activate and D2_used > 1e-9:
164
- t_star = float(np.clip(-D1 / D2_used, self.newton_lo, self.newton_hi))
165
- else:
166
- t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
167
-
168
- return theta_after_adam + t_star * delta
@@ -1,150 +0,0 @@
1
- """
2
- SoftOpt (PyTorch): a COMPLETE, standalone torch.optim.Optimizer -- not a
3
- wrapper. Use it exactly like torch.optim.Adam:
4
-
5
- opt = SoftOpt(model.parameters(), model, loss_fn, lr=1e-3)
6
- loss = opt.step(batch)
7
-
8
- One step() call does the Adam-style base update AND the Newton-style
9
- correction, using genuine forward-mode automatic differentiation
10
- (torch.func.jvp) to get an exact directional derivative and curvature of
11
- your loss along a random probe direction -- mathematically the same
12
- quantity a hand-written Klein-Maimon soft-number propagation would give
13
- (dual numbers are isomorphic to single-axis soft numbers per the book's
14
- own Appendix A.2), computed via PyTorch's own AD engine instead of
15
- hand-written soft-number code.
16
-
17
- Requirement (the "known computation graph" criterion, validated across
18
- nine domains): loss_fn(model, batch) must be an exactly re-evaluatable,
19
- differentiable function -- e.g. a full-batch loss over a fixed dataset,
20
- a physics/circuit simulator, a known projection model. If your loss is
21
- a small stochastic minibatch sample that changes meaning between calls
22
- (the classic deep-RL moving-target problem), this mechanism is not
23
- validated to help -- use plain Adam/SGD instead.
24
- """
25
- import numpy as np
26
- import torch
27
- import torch.func as tfunc
28
- from torch.optim import Optimizer
29
- from torch.nn.utils.stateless import _reparametrize_module
30
-
31
-
32
- class SoftOpt(Optimizer):
33
- def __init__(self, params, model, loss_fn, lr=1e-3, betas=(0.9, 0.999), eps=1e-8,
34
- newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3, h_fd=1e-4, seed=None,
35
- _test_frozen_constant=1.0):
36
- defaults = dict(lr=lr, betas=betas, eps=eps)
37
- super().__init__(params, defaults)
38
- self.model = model
39
- self.loss_fn = loss_fn
40
- self.newton_lo = newton_lo
41
- self.newton_hi = newton_hi
42
- self.eta_fallback = eta_fallback
43
- self.h_fd = h_fd
44
- self.rng = np.random.default_rng(seed)
45
- self._test_frozen_constant = _test_frozen_constant
46
-
47
- def _adam_update(self):
48
- for group in self.param_groups:
49
- b1, b2 = group['betas']
50
- for p in group['params']:
51
- if p.grad is None:
52
- continue
53
- state = self.state[p]
54
- if len(state) == 0:
55
- state['step'] = 0
56
- state['exp_avg'] = torch.zeros_like(p)
57
- state['exp_avg_sq'] = torch.zeros_like(p)
58
- state['step'] += 1
59
- state['exp_avg'].mul_(b1).add_(p.grad, alpha=1 - b1)
60
- state['exp_avg_sq'].mul_(b2).addcmul_(p.grad, p.grad, value=1 - b2)
61
- bc1 = 1 - b1 ** state['step']
62
- bc2 = 1 - b2 ** state['step']
63
- denom = (state['exp_avg_sq'] / bc2).sqrt().add_(group['eps'])
64
- p.data.addcdiv_(state['exp_avg'], denom, value=-group['lr'] / bc1)
65
-
66
- def step(self, batch, _test_magnitude_source=None, _test_foreign_sampler=None):
67
- """batch: whatever loss_fn(model, batch) needs. Returns the loss
68
- (a float) from the pre-correction forward pass.
69
- _test_* are TEST-ONLY hooks for the causal-validation harness --
70
- normal usage never sets these.
71
- """
72
- all_params = [p for group in self.param_groups for p in group['params']]
73
-
74
- # --- standard forward/backward for the Adam step ---
75
- with torch.enable_grad():
76
- self.zero_grad()
77
- loss = self.loss_fn(self.model, batch)
78
- loss.backward()
79
- loss_value = float(loss.detach())
80
-
81
- with torch.no_grad():
82
- self._adam_update()
83
-
84
- if _test_magnitude_source == 'plain':
85
- return loss_value
86
-
87
- # --- Newton-style correction via genuine forward-mode AD ---
88
- with torch.no_grad():
89
- named = {id(p): name for name, p in self.model.named_parameters()}
90
- all_named = dict(self.model.named_parameters())
91
- names = [n for n, p in all_named.items() if any(p is q for q in all_params)]
92
- flat0 = torch.cat([all_named[n].detach().reshape(-1) for n in names])
93
- n_total = flat0.numel()
94
- delta = torch.tensor(self.rng.choice([-1.0, 1.0], size=n_total).astype(np.float32))
95
-
96
- fixed_vals = {n: p.detach() for n, p in all_named.items() if n not in names}
97
-
98
- def make_scalar_loss(the_batch):
99
- def scalar_loss(flat):
100
- pd = {}
101
- idx = 0
102
- for n in names:
103
- p = all_named[n]
104
- nn_ = p.numel()
105
- pd[n] = flat[idx:idx + nn_].reshape(p.shape)
106
- idx += nn_
107
- pd.update(fixed_vals)
108
- with _reparametrize_module(self.model, pd):
109
- return self.loss_fn(self.model, the_batch)
110
- return scalar_loss
111
-
112
- def D1_D2_at(point_theta, point_delta, point_batch):
113
- scalar_loss = make_scalar_loss(point_batch)
114
- _, d1 = tfunc.jvp(scalar_loss, (point_theta,), (point_delta,))
115
- def d_along(fp):
116
- _, d = tfunc.jvp(scalar_loss, (fp,), (point_delta,))
117
- return d
118
- _, d2 = tfunc.jvp(d_along, (point_theta,), (point_delta,))
119
- return float(d1), float(d2)
120
-
121
- D1, D2_real = D1_D2_at(flat0, delta, batch)
122
- activate = D2_real > 1e-8
123
-
124
- if _test_magnitude_source in (None, 'real'):
125
- D2_used = D2_real
126
- elif _test_magnitude_source == 'frozen':
127
- D2_used = self._test_frozen_constant
128
- elif _test_magnitude_source in ('foreign_point_and_dir', 'foreign_point_same_dir'):
129
- theta_f, batch_f = _test_foreign_sampler(self.rng)
130
- delta_f = (torch.tensor(self.rng.choice([-1.0, 1.0], size=n_total).astype(np.float32))
131
- if _test_magnitude_source == 'foreign_point_and_dir' else delta)
132
- _, D2_f = D1_D2_at(theta_f, delta_f, batch_f)
133
- D2_used = abs(D2_f)
134
- else:
135
- raise ValueError(_test_magnitude_source)
136
-
137
- if activate and D2_used > 1e-9:
138
- t_star = float(np.clip(-D1 / D2_used, self.newton_lo, self.newton_hi))
139
- else:
140
- t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
141
-
142
- new_flat = flat0 + t_star * delta
143
- idx = 0
144
- for n in names:
145
- p = all_named[n]
146
- nn_ = p.numel()
147
- p.data.copy_(new_flat[idx:idx + nn_].reshape(p.shape))
148
- idx += nn_
149
-
150
- return loss_value
File without changes
File without changes
File without changes
File without changes
File without changes