softopt 0.6.0__tar.gz → 0.6.2__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.6.0
3
+ Version: 0.6.2
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
@@ -85,7 +85,7 @@ is the mechanism.
85
85
  But at 10 qubits both arms covered only a few percent of the way to the ground
86
86
  state. 1.5× of very little is still very little. **If the plateau is your
87
87
  problem, this is not the fix.** The scan and its replication are in
88
- `benchmarks/barren_plateau/`.
88
+ `benchmarks_barren_plateau/`.
89
89
 
90
90
  ## Installation
91
91
 
@@ -122,6 +122,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
122
122
 
123
123
  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.
124
124
 
125
+ ## Where the slope comes from: `slope="model"` or `slope="measured"`
126
+
127
+ Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
128
+
129
+ | | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
130
+ |---|---|---|
131
+ | slope from | your model's exact derivative | your readings, along the direction you probed |
132
+ | converges to | the **model's** optimum | the **real system's** optimum |
133
+ | choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
134
+
135
+ With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
136
+
137
+ ```python
138
+ from softopt import SoftOpt, soft_compile, spsa_gradient
139
+
140
+ opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
141
+
142
+ for step in range(num_steps):
143
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
144
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
145
+ ```
146
+
147
+ `measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
148
+
149
+ If your model is exact, you do not need either mode, or any readings: optimise the model directly.
150
+
125
151
  ## PyTorch
126
152
 
127
153
  If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
@@ -169,6 +195,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
169
195
  | BeH₂ | 94.7% | 5/5 |
170
196
  | HeH⁺ | 90.9% | 5/5 |
171
197
 
198
+ **Quantum chemistry on a real molecule, and against COBYLA**
199
+
200
+ | Test | Baseline | Result | Win rate |
201
+ |---|---|---|---|
202
+ | H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
203
+ | H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
204
+
205
+ Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
206
+
172
207
  **Quantum control**
173
208
 
174
209
  | Domain | Improvement | Win rate |
@@ -59,7 +59,7 @@ is the mechanism.
59
59
  But at 10 qubits both arms covered only a few percent of the way to the ground
60
60
  state. 1.5× of very little is still very little. **If the plateau is your
61
61
  problem, this is not the fix.** The scan and its replication are in
62
- `benchmarks/barren_plateau/`.
62
+ `benchmarks_barren_plateau/`.
63
63
 
64
64
  ## Installation
65
65
 
@@ -96,6 +96,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
96
96
 
97
97
  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.
98
98
 
99
+ ## Where the slope comes from: `slope="model"` or `slope="measured"`
100
+
101
+ Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
102
+
103
+ | | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
104
+ |---|---|---|
105
+ | slope from | your model's exact derivative | your readings, along the direction you probed |
106
+ | converges to | the **model's** optimum | the **real system's** optimum |
107
+ | choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
108
+
109
+ With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
110
+
111
+ ```python
112
+ from softopt import SoftOpt, soft_compile, spsa_gradient
113
+
114
+ opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
115
+
116
+ for step in range(num_steps):
117
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
118
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
119
+ ```
120
+
121
+ `measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
122
+
123
+ If your model is exact, you do not need either mode, or any readings: optimise the model directly.
124
+
99
125
  ## PyTorch
100
126
 
101
127
  If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
@@ -143,6 +169,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
143
169
  | BeH₂ | 94.7% | 5/5 |
144
170
  | HeH⁺ | 90.9% | 5/5 |
145
171
 
172
+ **Quantum chemistry on a real molecule, and against COBYLA**
173
+
174
+ | Test | Baseline | Result | Win rate |
175
+ |---|---|---|---|
176
+ | H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
177
+ | H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
178
+
179
+ Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
180
+
146
181
  **Quantum control**
147
182
 
148
183
  | Domain | Improvement | Win rate |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "softopt"
7
- version = "0.6.0"
7
+ version = "0.6.2"
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" }
@@ -3,7 +3,8 @@ SoftOpt — a standalone optimizer built on Klein–Maimon soft-number
3
3
  calculus, for problems with a known, differentiable computation graph
4
4
  (quantum circuits, physical models, projection models, ...).
5
5
 
6
- from softopt import SoftOpt # numpy version
6
+ from softopt import SoftOpt # numpy version (slope="model" or "measured")
7
+ from softopt import spsa_gradient # gradient, direction and measured slope from 2 readings
7
8
  from softopt import SoftOptTorch # PyTorch version (torch.optim.Optimizer)
8
9
  from softopt import soft_compile # turn a plain-Python model into g_delta
9
10
  from softopt import SoftNumber # the algebraic primitive itself
@@ -11,7 +12,7 @@ calculus, for problems with a known, differentiable computation graph
11
12
  See README.md for the exact requirement your problem must satisfy
12
13
  (the "known computation graph" criterion) and worked examples.
13
14
  """
14
- from .numpy_optimizer import SoftOpt
15
+ from .numpy_optimizer import SoftOpt, spsa_gradient
15
16
  from .compile import soft_compile
16
17
  from .soft_number import (
17
18
  SoftNumber, sadd, sneg, ssub, smul, sinv, sdiv,
@@ -28,8 +29,8 @@ def __getattr__(name):
28
29
 
29
30
 
30
31
  __all__ = [
31
- "SoftOpt", "SoftOptTorch", "SoftNumber", "soft_compile",
32
+ "SoftOpt", "SoftOptTorch", "SoftNumber", "soft_compile", "spsa_gradient",
32
33
  "sadd", "sneg", "ssub", "smul", "sinv", "sdiv",
33
34
  "spow", "sroot", "ssqrt", "ssin", "scos", "sexp", "slog",
34
35
  ]
35
- __version__ = "0.6.0"
36
+ __version__ = "0.6.2"
@@ -7,10 +7,25 @@ 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. 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.
10
+ The correction is a bounded Newton step, t* = -D1 / D2 along a direction delta.
11
+ D2 (the curvature) always comes from your model: a central finite difference
12
+ OF the exact soft-number derivative, with the usual step-size trade-off,
13
+ controlled by h_fd. D1 (the slope) has two possible sources, set by `slope`:
14
+
15
+ slope="model" (default) D1 is the model's exact directional derivative
16
+ along a fresh random direction. Robust to very noisy
17
+ readings, but the correction pulls towards the MODEL's
18
+ optimum: if the model is biased, so is the answer.
19
+
20
+ slope="measured" D1 is the slope you measured on the real system, along the
21
+ direction you measured it in (an SPSA difference gives you
22
+ both for free). The model only supplies the curvature, so
23
+ the fixed point is where the MEASURED slope vanishes: the
24
+ real system's optimum, even when the model is off. Needs
25
+ readings clean enough that the measured slope is informative.
26
+
27
+ `spsa_gradient` returns the gradient estimate, the direction and the measured
28
+ slope in one call, ready for either mode.
14
29
  """
15
30
  import numpy as np
16
31
 
@@ -18,7 +33,10 @@ import numpy as np
18
33
  class SoftOpt:
19
34
  def __init__(self, n_params, g_delta, lr=0.02, betas=(0.9, 0.999), eps=1e-8,
20
35
  newton_lo=-0.5, newton_hi=0.5, eta_fallback=0.3,
21
- h_fd=1e-4, seed=None, _test_frozen_constant=1.0):
36
+ h_fd=1e-4, seed=None, slope="model", _test_frozen_constant=1.0):
37
+ if slope not in ("model", "measured"):
38
+ raise ValueError(f'slope must be "model" or "measured", got {slope!r}')
39
+ self.slope = slope
22
40
  self.g_delta = g_delta
23
41
  self.lr = lr
24
42
  self.b1, self.b2 = betas
@@ -34,13 +52,19 @@ class SoftOpt:
34
52
  self._ablation_rng = None
35
53
  self._test_frozen_constant = _test_frozen_constant
36
54
 
37
- def step(self, theta, grad_estimate, _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
55
+ def step(self, theta, grad_estimate, direction=None, measured_slope=None,
56
+ _test_magnitude_source=None, _test_foreign_sampler=None, _test_rng=None):
38
57
  """One complete optimizer step: Adam base update + Newton-style
39
58
  correction from the known model, in a single call.
40
59
  theta: current parameters (np.ndarray)
41
60
  grad_estimate: a gradient estimate of the loss w.r.t. theta (e.g.
42
61
  from SPSA or backprop) -- exactly what you'd normally hand
43
62
  to Adam.
63
+ direction, measured_slope: required when slope="measured", ignored
64
+ otherwise. The direction your readings probed and the slope you
65
+ measured along it at theta, e.g. for SPSA
66
+ (f(theta + c*d) - f(theta - c*d)) / (2c) along d.
67
+ spsa_gradient() returns both.
44
68
  Returns: theta for the next step.
45
69
 
46
70
  The _test_* arguments are TEST-ONLY hooks used exclusively by the
@@ -53,6 +77,13 @@ class SoftOpt:
53
77
  """
54
78
  theta = np.asarray(theta, dtype=float)
55
79
  grad_estimate = np.asarray(grad_estimate, dtype=float)
80
+ if self.slope == "measured":
81
+ if direction is None or measured_slope is None:
82
+ raise ValueError('slope="measured" needs direction= and measured_slope= on every '
83
+ 'step (spsa_gradient() returns both)')
84
+ direction = np.asarray(direction, dtype=float)
85
+ if direction.shape != theta.shape:
86
+ raise ValueError(f"direction has shape {direction.shape}, theta has {theta.shape}")
56
87
 
57
88
  # --- Adam base update ---
58
89
  self.t += 1
@@ -70,8 +101,11 @@ class SoftOpt:
70
101
 
71
102
  # --- correction from the known, exact model ---
72
103
  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)
104
+ if self.slope == "measured":
105
+ delta = direction
106
+ else:
107
+ gen_rng = _test_rng if _test_rng is not None else self.rng
108
+ delta = gen_rng.choice([-1.0, 1.0], size=n)
75
109
 
76
110
  # The ablation arms must not disturb the probe-direction stream, or
77
111
  # a corrupted arm would also change every direction that follows and
@@ -79,7 +113,10 @@ class SoftOpt:
79
113
  if self._ablation_rng is None:
80
114
  self._ablation_rng = np.random.default_rng(
81
115
  self.rng.integers(0, 2**63 - 1))
82
- D1 = self.g_delta(theta_after_adam, delta)
116
+ if self.slope == "measured":
117
+ D1 = float(measured_slope) # from the real system
118
+ else:
119
+ D1 = self.g_delta(theta_after_adam, delta)
83
120
  gp = self.g_delta(theta_after_adam + self.h_fd * delta, delta)
84
121
  gm = self.g_delta(theta_after_adam - self.h_fd * delta, delta)
85
122
  D2_real = (gp - gm) / (2 * self.h_fd)
@@ -104,3 +141,18 @@ class SoftOpt:
104
141
  t_star = float(np.clip(-self.eta_fallback * D1, self.newton_lo, self.newton_hi))
105
142
 
106
143
  return theta_after_adam + t_star * delta
144
+
145
+
146
+ def spsa_gradient(measure, theta, c=0.1, rng=None):
147
+ """One SPSA probe of a noisy objective.
148
+
149
+ measure: callable theta -> one (noisy) reading of the real system.
150
+ Returns (grad_estimate, direction, measured_slope), where measured_slope
151
+ is (measure(theta + c*d) - measure(theta - c*d)) / (2c) along the random
152
+ +-1 direction d, and grad_estimate = measured_slope * d. Costs 2 readings.
153
+ """
154
+ theta = np.asarray(theta, dtype=float)
155
+ rng = np.random.default_rng() if rng is None else rng
156
+ d = rng.choice([-1.0, 1.0], size=theta.shape)
157
+ slope = (measure(theta + c * d) - measure(theta - c * d)) / (2 * c)
158
+ return slope * d, d, float(slope)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -85,7 +85,7 @@ is the mechanism.
85
85
  But at 10 qubits both arms covered only a few percent of the way to the ground
86
86
  state. 1.5× of very little is still very little. **If the plateau is your
87
87
  problem, this is not the fix.** The scan and its replication are in
88
- `benchmarks/barren_plateau/`.
88
+ `benchmarks_barren_plateau/`.
89
89
 
90
90
  ## Installation
91
91
 
@@ -122,6 +122,32 @@ The one thing to avoid is converting a traced value back to a plain number mid-m
122
122
 
123
123
  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.
124
124
 
125
+ ## Where the slope comes from: `slope="model"` or `slope="measured"`
126
+
127
+ Every correction is a Newton step, `-slope / curvature`, taken along one direction. The curvature always comes from your model. The slope has two possible sources:
128
+
129
+ | | `slope="model"` (default) | `slope="measured"` (new in 0.6.1) |
130
+ |---|---|---|
131
+ | slope from | your model's exact derivative | your readings, along the direction you probed |
132
+ | converges to | the **model's** optimum | the **real system's** optimum |
133
+ | choose it when | the model is accurate and the readings are very noisy | the model may be biased, and the readings are clean enough to show the slope |
134
+
135
+ With `slope="model"`, a model whose optimum is in the wrong place pulls the answer to the wrong place. `slope="measured"` uses the model only for the shape of the landscape, so the readings decide where the optimum is. An SPSA probe already measures that slope, so the measured mode costs no extra readings:
136
+
137
+ ```python
138
+ from softopt import SoftOpt, soft_compile, spsa_gradient
139
+
140
+ opt = SoftOpt(n, soft_compile(my_model, n_params=n), lr=0.05, slope="measured")
141
+
142
+ for step in range(num_steps):
143
+ grad, direction, slope = spsa_gradient(measure, theta, c=0.1) # 2 readings
144
+ theta = opt.step(theta, grad, direction=direction, measured_slope=slope)
145
+ ```
146
+
147
+ `measure(theta)` returns one reading of the real system. If you already compute your own SPSA estimate, pass its direction `d` and `(f(θ+cd) - f(θ-cd)) / 2c` as `measured_slope`.
148
+
149
+ If your model is exact, you do not need either mode, or any readings: optimise the model directly.
150
+
125
151
  ## PyTorch
126
152
 
127
153
  If your known model is written in PyTorch, `SoftOptTorch` runs the same step as a `torch.optim.Optimizer`: an Adam update from the gradient, then the bounded Newton correction. The directional derivative and the curvature along the probe direction come from PyTorch's forward-mode AD (`torch.func.jvp`, nested once), which is single-axis soft-number propagation executed by PyTorch's own engine. It runs wherever your model runs: CPU, CUDA or Apple MPS.
@@ -169,6 +195,15 @@ All results below use IBM's `FakeFez` noise model via Qiskit + Aer, or realistic
169
195
  | BeH₂ | 94.7% | 5/5 |
170
196
  | HeH⁺ | 90.9% | 5/5 |
171
197
 
198
+ **Quantum chemistry on a real molecule, and against COBYLA**
199
+
200
+ | Test | Baseline | Result | Win rate |
201
+ |---|---|---|---|
202
+ | H₄ molecule (STO-3G, 8 qubits, 185 Pauli terms), 100 steps | Adam | 67% smaller gap to the ground state (409 vs 1227 mHa) | 10/10 |
203
+ | H₂, same budget of 180 device readings | COBYLA | 0.11 vs 5.81 mHa above the ground state; chemical accuracy in 10/10 runs vs 3/10 | 10/10 |
204
+
205
+ Both run SoftOpt with its default settings on IBM's `FakeFez` noise model: `benchmarks/vqe/h4_real_molecule.py`, `benchmarks/vqe/h2_vs_cobyla.py`.
206
+
172
207
  **Quantum control**
173
208
 
174
209
  | Domain | Improvement | Win rate |
@@ -14,5 +14,6 @@ softopt.egg-info/top_level.txt
14
14
  tests/test_basic.py
15
15
  tests/test_causal.py
16
16
  tests/test_compile.py
17
+ tests/test_measured_slope.py
17
18
  tests/test_soft_number.py
18
19
  tests/test_torch.py
@@ -0,0 +1,77 @@
1
+ """slope="measured": slope from the real system, curvature from the model."""
2
+ import numpy as np
3
+ import pytest
4
+ from softopt import SoftOpt, soft_compile, spsa_gradient
5
+
6
+ N = 6
7
+ SHIFT = np.array([0.8, -0.6, 0.5, -0.4, 0.7, -0.5]) # the model's optimum is off by this
8
+
9
+
10
+ def truth(x):
11
+ return float(np.sum(np.asarray(x) ** 2))
12
+
13
+
14
+ def biased_model(th):
15
+ return sum((t - s) ** 2 for t, s in zip(th, SHIFT))
16
+
17
+
18
+ def _run(slope, steps=400, seed=0):
19
+ g_delta = soft_compile(biased_model, n_params=N)
20
+ opt = SoftOpt(N, g_delta, lr=0.05, seed=seed, slope=slope)
21
+ rng = np.random.default_rng(seed)
22
+ x = np.full(N, 2.0)
23
+ for _ in range(steps):
24
+ g, d, s = spsa_gradient(truth, x, c=0.1, rng=rng)
25
+ x = opt.step(x, g, direction=d, measured_slope=s)
26
+ return x
27
+
28
+
29
+ def test_measured_slope_finds_the_real_optimum_despite_a_biased_model():
30
+ x = _run("measured")
31
+ assert np.allclose(x, 0.0, atol=0.05) # the system's optimum
32
+
33
+
34
+ def test_model_slope_follows_the_model():
35
+ x = _run("model")
36
+ # documented behaviour: the correction pulls towards the model's optimum
37
+ assert np.linalg.norm(x - SHIFT) < np.linalg.norm(x - 0.0)
38
+
39
+
40
+ def test_measured_mode_requires_direction_and_slope():
41
+ opt = SoftOpt(N, soft_compile(biased_model, n_params=N), slope="measured")
42
+ with pytest.raises(ValueError, match="direction"):
43
+ opt.step(np.ones(N), np.ones(N))
44
+ with pytest.raises(ValueError, match="shape"):
45
+ opt.step(np.ones(N), np.ones(N), direction=np.ones(N + 1), measured_slope=1.0)
46
+
47
+
48
+ def test_unknown_slope_mode_rejected():
49
+ with pytest.raises(ValueError, match="slope"):
50
+ SoftOpt(N, lambda t, d: 0.0, slope="both")
51
+
52
+
53
+ def test_model_mode_ignores_direction_arguments():
54
+ g_delta = soft_compile(biased_model, n_params=N)
55
+ a = SoftOpt(N, g_delta, lr=0.05, seed=3)
56
+ b = SoftOpt(N, g_delta, lr=0.05, seed=3)
57
+ x = np.ones(N)
58
+ xa = a.step(x, 2 * x)
59
+ xb = b.step(x, 2 * x, direction=-np.ones(N), measured_slope=123.0)
60
+ assert np.array_equal(xa, xb)
61
+
62
+
63
+ def test_spsa_gradient_on_a_linear_function_is_exact():
64
+ w = np.arange(1.0, N + 1)
65
+ g, d, s = spsa_gradient(lambda x: float(w @ x), np.zeros(N), c=0.3, rng=np.random.default_rng(0))
66
+ assert s == pytest.approx(w @ d)
67
+ assert np.allclose(g, s * d)
68
+ assert set(np.unique(d)) <= {-1.0, 1.0}
69
+
70
+
71
+ def test_frozen_ablation_hook_works_in_measured_mode():
72
+ g_delta = soft_compile(biased_model, n_params=N)
73
+ opt = SoftOpt(N, g_delta, lr=0.05, seed=0, slope="measured")
74
+ x = np.ones(N)
75
+ g, d, s = spsa_gradient(truth, x, rng=np.random.default_rng(1))
76
+ out = opt.step(x, g, direction=d, measured_slope=s, _test_magnitude_source="frozen")
77
+ assert out.shape == x.shape and np.all(np.isfinite(out))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes