softopt 0.2.0__tar.gz → 0.3.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -49,8 +49,8 @@ Full reinforcement learning (or anything else with a continuously **moving targe
49
49
  ## Installation
50
50
 
51
51
  ```bash
52
- pip install softopt # numpy version only
53
- pip install softopt[torch] # + the PyTorch optimizer
52
+ pip install softopt # numpy version only
53
+ pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
54
54
  ```
55
55
 
56
56
  ## Quick start
@@ -58,19 +58,31 @@ pip install softopt[torch] # + the PyTorch optimizer
58
58
  ### NumPy
59
59
 
60
60
  ```python
61
- from softopt import SoftOpt
61
+ import numpy as np
62
+ from softopt import SoftOpt, soft_compile
62
63
 
63
- # g_delta(theta, delta) must return the EXACT directional derivative of
64
- # your true, differentiable objective along `delta`, at `theta` — computed
65
- # from your own known model (a circuit, a projection, a physical law),
66
- # not estimated from noisy measurements.
67
- opt = SoftOpt(n_params, g_delta, lr=0.02)
64
+ # Write your model once, in plain Python. No special arithmetic.
65
+ def my_model(theta):
66
+ x, y = theta
67
+ return (x - 2.0)**2 + np.exp(y) * x
68
+
69
+ # soft_compile turns it into the exact directional-derivative function
70
+ # SoftOpt needs, and verifies the result against your own model.
71
+ g_delta = soft_compile(my_model, n_params=2)
72
+
73
+ opt = SoftOpt(2, g_delta, lr=0.02)
68
74
 
69
75
  for step in range(num_steps):
70
76
  grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
71
77
  theta = opt.step(theta, grad_estimate)
72
78
  ```
73
79
 
80
+ Your model needs to be built from arithmetic and the elementary functions the soft algebra defines: `+ - * / **` (including negative and fractional exponents, and `2.0 ** x`), `abs`, `sqrt`, `exp`, `log`, `log10`, `sin`, `cos`, `tanh`, comparisons, and numpy's versions of those. Loops and `if`/`else` branching on parameter values work too — the compiled derivative is then valid on the branch your parameters are actually in.
81
+
82
+ The one thing to avoid is converting a traced value back to a plain number mid-model — `float(x)`, or `np.array(params)` on the parameter list, both silently discard the derivative. `soft_compile`'s verification catches this and says so.
83
+
84
+ 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
+
74
86
  ### PyTorch
75
87
 
76
88
  ```python
@@ -22,8 +22,8 @@ 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
25
+ pip install softopt # numpy version only
26
+ pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
27
27
  ```
28
28
 
29
29
  ## Quick start
@@ -31,19 +31,31 @@ pip install softopt[torch] # + the PyTorch optimizer
31
31
  ### NumPy
32
32
 
33
33
  ```python
34
- from softopt import SoftOpt
34
+ import numpy as np
35
+ from softopt import SoftOpt, soft_compile
35
36
 
36
- # g_delta(theta, delta) must return the EXACT directional derivative of
37
- # your true, differentiable objective along `delta`, at `theta` — computed
38
- # from your own known model (a circuit, a projection, a physical law),
39
- # not estimated from noisy measurements.
40
- opt = SoftOpt(n_params, g_delta, lr=0.02)
37
+ # Write your model once, in plain Python. No special arithmetic.
38
+ def my_model(theta):
39
+ x, y = theta
40
+ return (x - 2.0)**2 + np.exp(y) * x
41
+
42
+ # soft_compile turns it into the exact directional-derivative function
43
+ # SoftOpt needs, and verifies the result against your own model.
44
+ g_delta = soft_compile(my_model, n_params=2)
45
+
46
+ opt = SoftOpt(2, g_delta, lr=0.02)
41
47
 
42
48
  for step in range(num_steps):
43
49
  grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
44
50
  theta = opt.step(theta, grad_estimate)
45
51
  ```
46
52
 
53
+ Your model needs to be built from arithmetic and the elementary functions the soft algebra defines: `+ - * / **` (including negative and fractional exponents, and `2.0 ** x`), `abs`, `sqrt`, `exp`, `log`, `log10`, `sin`, `cos`, `tanh`, comparisons, and numpy's versions of those. Loops and `if`/`else` branching on parameter values work too — the compiled derivative is then valid on the branch your parameters are actually in.
54
+
55
+ The one thing to avoid is converting a traced value back to a plain number mid-model — `float(x)`, or `np.array(params)` on the parameter list, both silently discard the derivative. `soft_compile`'s verification catches this and says so.
56
+
57
+ 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
+
47
59
  ### PyTorch
48
60
 
49
61
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "softopt"
7
- version = "0.2.0"
7
+ version = "0.3.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" }
@@ -5,23 +5,25 @@ calculus, for problems with a known, differentiable computation graph
5
5
 
6
6
  from softopt import SoftOpt # numpy version
7
7
  from softopt import SoftOptTorch # PyTorch version (torch.optim.Optimizer)
8
+ from softopt import soft_compile # turn a plain-Python model into g_delta
8
9
  from softopt import SoftNumber # the algebraic primitive itself
9
10
 
10
11
  See README.md for the exact requirement your problem must satisfy
11
12
  (the "known computation graph" criterion) and worked examples.
12
13
  """
13
14
  from .numpy_optimizer import SoftOpt
15
+ from .compile import soft_compile
14
16
  from .soft_number import (
15
17
  SoftNumber, sadd, sneg, ssub, smul, sinv, sdiv,
16
- spow, sroot, ssqrt, ssin, scos, sexp,
18
+ spow, sroot, ssqrt, ssin, scos, sexp, slog,
17
19
  )
18
20
 
19
21
  __all__ = [
20
- "SoftOpt", "SoftNumber",
22
+ "SoftOpt", "SoftNumber", "soft_compile",
21
23
  "sadd", "sneg", "ssub", "smul", "sinv", "sdiv",
22
- "spow", "sroot", "ssqrt", "ssin", "scos", "sexp",
24
+ "spow", "sroot", "ssqrt", "ssin", "scos", "sexp", "slog",
23
25
  ]
24
- __version__ = "0.2.0"
26
+ __version__ = "0.3.0"
25
27
 
26
28
  try:
27
29
  from .torch_optimizer import SoftOpt as SoftOptTorch
@@ -0,0 +1,101 @@
1
+ """
2
+ soft_compile: turn a plain-Python model into the exact directional-
3
+ derivative function (`g_delta`) that SoftOpt needs -- without writing any
4
+ soft-number arithmetic yourself.
5
+
6
+ from softopt import SoftOpt, soft_compile
7
+
8
+ def my_model(theta): # plain Python. numpy works too.
9
+ x, y = theta
10
+ return x**2 + np.exp(y) * x
11
+
12
+ g_delta = soft_compile(my_model, n_params=2)
13
+ opt = SoftOpt(2, g_delta, lr=0.02)
14
+
15
+ Your model is traced once with SoftNumber objects in place of floats, so
16
+ every operation carries its exact derivative alongside its value. This is
17
+ exact -- not a finite-difference approximation.
18
+
19
+ Your model must be built from arithmetic and the elementary functions the
20
+ soft algebra defines (+ - * / ** sqrt exp sin cos, and numpy's versions of
21
+ those). Branching on a parameter's *value* (`if x > 0:`) traces only the
22
+ branch taken at that point, which is correct locally but means the
23
+ compiled function is valid piecewise -- see `verify=True`.
24
+ """
25
+ import numpy as np
26
+
27
+ from .soft_number import SoftNumber
28
+
29
+
30
+ def soft_compile(model_fn, n_params=None, verify=True, verify_tol=1e-5):
31
+ """model_fn: takes a sequence of scalars, returns a scalar.
32
+ n_params: needed only for verification.
33
+ verify: check the compiled derivative against finite differences at a
34
+ random point before returning it. Catches the common mistakes --
35
+ a model that isn't actually differentiable, uses an unsupported
36
+ operation, or silently drops the traced parameters."""
37
+ def g_delta(theta, delta):
38
+ soft = [SoftNumber(float(d), float(t)) for t, d in zip(theta, delta)]
39
+ result = model_fn(soft)
40
+ if not isinstance(result, SoftNumber):
41
+ raise TypeError(
42
+ "the compiled model returned a plain number, not a SoftNumber "
43
+ "-- its output doesn't depend on the traced parameters, or an "
44
+ "operation in it discarded them (e.g. float(), np.array(...), "
45
+ "or comparison-based rounding)"
46
+ )
47
+ return result.a
48
+
49
+ if verify:
50
+ if n_params is None:
51
+ raise ValueError("pass n_params to verify the compiled model, "
52
+ "or verify=False to skip the check")
53
+ _verify(model_fn, g_delta, n_params, verify_tol)
54
+
55
+ return g_delta
56
+
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."""
61
+ rng = np.random.default_rng(0)
62
+ for attempt in range(5):
63
+ theta = rng.normal(0.5, 1.0, n_params)
64
+ direction = rng.normal(0, 1, n_params)
65
+ try:
66
+ exact = g_delta(theta, direction)
67
+ except ZeroDivisionError:
68
+ continue # landed on a singular point; try another
69
+ h = 1e-6
70
+ try:
71
+ f_plus = float(model_fn(list(theta + h * direction)))
72
+ f_minus = float(model_fn(list(theta - h * direction)))
73
+ except Exception as e:
74
+ raise RuntimeError(
75
+ f"the model raised {type(e).__name__} when called with plain "
76
+ f"floats: {e}. soft_compile needs a model that works with both "
77
+ "floats and SoftNumbers."
78
+ ) from e
79
+ approx = (f_plus - f_minus) / (2 * h)
80
+
81
+ scale = max(abs(approx), abs(exact), 1.0)
82
+ if abs(exact - approx) / scale < tol:
83
+ return # verified
84
+ if abs(approx) < 1e-12 and abs(exact) < 1e-12:
85
+ continue # flat direction here; uninformative, try another
86
+
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."
93
+ )
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
+ )
@@ -56,13 +56,21 @@ def sdiv(x, y):
56
56
 
57
57
  # --- Section 3.3.4: Lemma 3.3, integer powers ---
58
58
  def spow(x, n):
59
- if not isinstance(n, int) or n < 0:
60
- raise ValueError("spow implements Lemma 3.3 for non-negative "
61
- "integers only; use sroot(x, n) for Lemma 3.4/3.5")
59
+ """Integer powers via Lemma 3.3; fractional powers by composing
60
+ Lemma 3.3 with Lemma 3.5's root (e.g. x**2.5 = (x**5)**(1/2))."""
62
61
  a, b = x
63
- if n == 0:
64
- return (0.0, 1.0)
65
- return (n * a * b ** (n - 1), b**n)
62
+ if isinstance(n, float) and n.is_integer():
63
+ n = int(n)
64
+ if isinstance(n, int):
65
+ if n < 0:
66
+ return sinv(spow(x, -n))
67
+ if n == 0:
68
+ return (0.0, 1.0)
69
+ return (n * a * b ** (n - 1), b**n)
70
+ # fractional: x**(p/q). Use the chain rule form directly -- for
71
+ # f(t)=t^n the extension rule gives (a*n*b^(n-1), b^n), which holds
72
+ # for real n as well, by Lemma 6.1's general f(a,b)=(a*f'(b), f(b)).
73
+ return (a * n * b ** (n - 1), b**n)
66
74
 
67
75
 
68
76
  # --- Section 3.3.5: Lemma 3.4 (square root), Lemma 3.5 (n-th root) ---
@@ -106,6 +114,14 @@ def sexp(x):
106
114
  return (a * e, e)
107
115
 
108
116
 
117
+ def slog(x):
118
+ """Natural logarithm, Lemma 6.1's general rule with f(t)=ln(t)."""
119
+ a, b = x
120
+ if b <= 0:
121
+ raise ValueError("slog requires a positive real component")
122
+ return (a / b, np.log(b))
123
+
124
+
109
125
  class SoftNumber:
110
126
  """Object wrapper around the tuple functions above -- same math,
111
127
  operator syntax. See module docstring."""
@@ -157,8 +173,58 @@ class SoftNumber:
157
173
  return self._coerce(other) / self
158
174
 
159
175
  def __pow__(self, n):
176
+ if isinstance(n, SoftNumber):
177
+ # x**y with both soft: x**y = exp(y * ln x)
178
+ return (self.log() * n).exp()
160
179
  return self._from_tuple(spow(self.as_tuple(), n))
161
180
 
181
+ def __rpow__(self, base):
182
+ """base ** self, where base is an ordinary number.
183
+ d/dt base^t = base^t * ln(base)."""
184
+ if base <= 0:
185
+ raise ValueError(f"{base}**SoftNumber requires a positive base")
186
+ val = base ** self.b
187
+ return SoftNumber(self.a * val * np.log(base), val)
188
+
189
+ def log(self):
190
+ """Natural log, via Lemma 6.1's general rule with f=ln."""
191
+ return self._from_tuple(slog(self.as_tuple()))
192
+
193
+ def tanh(self):
194
+ t = np.tanh(self.b)
195
+ return SoftNumber(self.a * (1.0 - t * t), t)
196
+
197
+ def log10(self):
198
+ return SoftNumber(self.a / (self.b * np.log(10.0)), np.log10(self.b))
199
+
200
+ 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)."""
203
+ if self.b < 0:
204
+ return -self
205
+ return SoftNumber(self.a, self.b)
206
+
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.
210
+ def __lt__(self, other):
211
+ return self.b < self._coerce(other).b
212
+
213
+ def __le__(self, other):
214
+ return self.b <= self._coerce(other).b
215
+
216
+ def __gt__(self, other):
217
+ return self.b > self._coerce(other).b
218
+
219
+ def __ge__(self, other):
220
+ return self.b >= self._coerce(other).b
221
+
222
+ def __float__(self):
223
+ """The real component. Note this DISCARDS the derivative -- if your
224
+ model calls float() on a traced value, the compiled derivative will
225
+ be wrong, which soft_compile's verification is designed to catch."""
226
+ return float(self.b)
227
+
162
228
  def root(self, n):
163
229
  return self._from_tuple(sroot(self.as_tuple(), n))
164
230
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: softopt
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -49,8 +49,8 @@ Full reinforcement learning (or anything else with a continuously **moving targe
49
49
  ## Installation
50
50
 
51
51
  ```bash
52
- pip install softopt # numpy version only
53
- pip install softopt[torch] # + the PyTorch optimizer
52
+ pip install softopt # numpy version only
53
+ pip install "softopt[torch]" # + the PyTorch optimizer (quotes needed on zsh, macOS's default shell)
54
54
  ```
55
55
 
56
56
  ## Quick start
@@ -58,19 +58,31 @@ pip install softopt[torch] # + the PyTorch optimizer
58
58
  ### NumPy
59
59
 
60
60
  ```python
61
- from softopt import SoftOpt
61
+ import numpy as np
62
+ from softopt import SoftOpt, soft_compile
62
63
 
63
- # g_delta(theta, delta) must return the EXACT directional derivative of
64
- # your true, differentiable objective along `delta`, at `theta` — computed
65
- # from your own known model (a circuit, a projection, a physical law),
66
- # not estimated from noisy measurements.
67
- opt = SoftOpt(n_params, g_delta, lr=0.02)
64
+ # Write your model once, in plain Python. No special arithmetic.
65
+ def my_model(theta):
66
+ x, y = theta
67
+ return (x - 2.0)**2 + np.exp(y) * x
68
+
69
+ # soft_compile turns it into the exact directional-derivative function
70
+ # SoftOpt needs, and verifies the result against your own model.
71
+ g_delta = soft_compile(my_model, n_params=2)
72
+
73
+ opt = SoftOpt(2, g_delta, lr=0.02)
68
74
 
69
75
  for step in range(num_steps):
70
76
  grad_estimate = my_gradient_estimate(theta) # e.g. from SPSA on real hardware
71
77
  theta = opt.step(theta, grad_estimate)
72
78
  ```
73
79
 
80
+ Your model needs to be built from arithmetic and the elementary functions the soft algebra defines: `+ - * / **` (including negative and fractional exponents, and `2.0 ** x`), `abs`, `sqrt`, `exp`, `log`, `log10`, `sin`, `cos`, `tanh`, comparisons, and numpy's versions of those. Loops and `if`/`else` branching on parameter values work too — the compiled derivative is then valid on the branch your parameters are actually in.
81
+
82
+ The one thing to avoid is converting a traced value back to a plain number mid-model — `float(x)`, or `np.array(params)` on the parameter list, both silently discard the derivative. `soft_compile`'s verification catches this and says so.
83
+
84
+ 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
+
74
86
  ### PyTorch
75
87
 
76
88
  ```python
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  softopt/__init__.py
5
+ softopt/compile.py
5
6
  softopt/numpy_optimizer.py
6
7
  softopt/soft_number.py
7
8
  softopt/torch_optimizer.py
@@ -11,4 +12,5 @@ softopt.egg-info/dependency_links.txt
11
12
  softopt.egg-info/requires.txt
12
13
  softopt.egg-info/top_level.txt
13
14
  tests/test_basic.py
15
+ tests/test_compile.py
14
16
  tests/test_soft_number.py
@@ -0,0 +1,147 @@
1
+ """
2
+ Tests for soft_compile -- generating g_delta from a plain-Python model.
3
+ """
4
+ import numpy as np
5
+ import pytest
6
+ from softopt import soft_compile, SoftOpt
7
+
8
+
9
+ def test_polynomial_matches_finite_difference():
10
+ def model(p):
11
+ x, y, z = p
12
+ return x * x + y * y * y + x * y * z
13
+
14
+ g = soft_compile(model, n_params=3)
15
+ theta = np.array([1.3, -0.7, 2.1])
16
+ direction = np.array([1.0, -1.0, 1.0])
17
+ h = 1e-6
18
+ approx = (model(list(theta + h * direction)) - model(list(theta - h * direction))) / (2 * h)
19
+ assert np.isclose(g(theta, direction), approx, rtol=1e-6)
20
+
21
+
22
+ def test_elementary_functions_work():
23
+ def model(p):
24
+ x, y = p
25
+ return np.exp(x) * np.sin(y) + np.sqrt(x * x + 1.0)
26
+
27
+ g = soft_compile(model, n_params=2)
28
+ theta = np.array([0.4, 1.1])
29
+ direction = np.array([1.0, -1.0])
30
+ h = 1e-6
31
+ approx = (model(list(theta + h * direction)) - model(list(theta - h * direction))) / (2 * h)
32
+ assert np.isclose(g(theta, direction), approx, rtol=1e-5)
33
+
34
+
35
+ def test_fractional_and_negative_powers():
36
+ def model(p):
37
+ x, = p
38
+ return x ** 2.5 + x ** -1
39
+
40
+ g = soft_compile(model, n_params=1)
41
+ theta = np.array([1.7])
42
+ direction = np.array([1.0])
43
+ h = 1e-6
44
+ approx = (model(list(theta + h * direction)) - model(list(theta - h * direction))) / (2 * h)
45
+ assert np.isclose(g(theta, direction), approx, rtol=1e-5)
46
+
47
+
48
+ def test_model_ignoring_params_is_rejected():
49
+ def model(p):
50
+ return 5.0
51
+
52
+ with pytest.raises(TypeError, match="doesn't depend on the traced parameters"):
53
+ soft_compile(model, n_params=2)
54
+
55
+
56
+ def test_verify_requires_n_params():
57
+ with pytest.raises(ValueError, match="pass n_params"):
58
+ soft_compile(lambda p: p[0] * p[0])
59
+
60
+
61
+ def test_verify_can_be_skipped():
62
+ g = soft_compile(lambda p: p[0] * p[0], verify=False)
63
+ assert np.isclose(g(np.array([3.0]), np.array([1.0])), 6.0)
64
+
65
+
66
+ def test_compiled_model_drives_the_optimizer():
67
+ """End-to-end: compile a model, optimize with it, reach the minimum."""
68
+ def model(p):
69
+ x, y = p
70
+ return (x - 2.0) ** 2 + (y + 1.0) ** 2
71
+
72
+ g = soft_compile(model, n_params=2)
73
+ opt = SoftOpt(2, g, lr=0.1, seed=0)
74
+ theta = np.array([0.0, 0.0])
75
+ for _ in range(300):
76
+ grad = np.array([2 * (theta[0] - 2.0), 2 * (theta[1] + 1.0)])
77
+ theta = opt.step(theta, grad)
78
+ assert np.allclose(theta, [2.0, -1.0], atol=1e-2)
79
+
80
+
81
+ def _fd_check(model, n, seed=77):
82
+ from softopt import soft_compile
83
+ g = soft_compile(model, n_params=n)
84
+ rng = np.random.default_rng(seed)
85
+ th = rng.normal(0.4, 0.9, n)
86
+ dl = rng.normal(0, 1, n)
87
+ exact = g(th, dl)
88
+ h = 1e-6
89
+ fd = (float(model(list(th + h * dl))) - float(model(list(th - h * dl)))) / (2 * h)
90
+ scale = max(abs(fd), abs(exact), 1.0)
91
+ assert abs(exact - fd) / scale < 1e-4, f"exact={exact} fd={fd}"
92
+
93
+
94
+ def test_loops_over_parameters():
95
+ def model(p):
96
+ x, = p
97
+ total = x * 0.0
98
+ for i in range(5):
99
+ total = total + x ** (i + 1) / (i + 1)
100
+ return total
101
+ _fd_check(model, 1)
102
+
103
+
104
+ def test_branching_on_parameter_value():
105
+ def model(p):
106
+ x, = p
107
+ return x * x if x > 0 else -x * x * x
108
+ _fd_check(model, 1)
109
+
110
+
111
+ def test_abs_and_max():
112
+ def model(p):
113
+ x, y = p
114
+ return abs(x) + max(y, 0.0)
115
+ _fd_check(model, 2)
116
+
117
+
118
+ def test_numpy_log_and_tanh():
119
+ def model(p):
120
+ x, = p
121
+ return np.log(np.exp(x) + 2.0) + np.tanh(x)
122
+ _fd_check(model, 1)
123
+
124
+
125
+ def test_base_to_the_power_of_a_parameter():
126
+ def model(p):
127
+ x, = p
128
+ return 2.0 ** x + np.e ** x
129
+ _fd_check(model, 1)
130
+
131
+
132
+ def test_object_array_quadratic_form():
133
+ def model(p):
134
+ v = np.array(p, dtype=object)
135
+ M = np.array([[1.0, 0.5], [0.5, 2.0]])
136
+ return sum(v[i] * M[i, j] * v[j] for i in range(2) for j in range(2))
137
+ _fd_check(model, 2)
138
+
139
+
140
+ def test_model_that_casts_to_float_is_caught():
141
+ """np.array(p) on a list of SoftNumbers silently converts to floats,
142
+ dropping the derivative -- verification must catch this."""
143
+ def model(p):
144
+ v = np.array(p) # drops the soft components
145
+ return float(v @ v)
146
+ with pytest.raises(TypeError, match="doesn't depend on the traced parameters"):
147
+ soft_compile(model, n_params=2)
File without changes
File without changes
File without changes