freealg 0.7.11__py3-none-any.whl → 0.7.14__py3-none-any.whl

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.
Files changed (36) hide show
  1. freealg/__init__.py +2 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +2 -1
  4. freealg/_algebraic_form/_constraints.py +53 -12
  5. freealg/_algebraic_form/_cusp.py +357 -0
  6. freealg/_algebraic_form/_cusp_wrap.py +268 -0
  7. freealg/_algebraic_form/_decompress.py +330 -381
  8. freealg/_algebraic_form/_decompress2.py +120 -0
  9. freealg/_algebraic_form/_decompress4.py +739 -0
  10. freealg/_algebraic_form/_decompress5.py +738 -0
  11. freealg/_algebraic_form/_decompress6.py +492 -0
  12. freealg/_algebraic_form/_decompress7.py +355 -0
  13. freealg/_algebraic_form/_decompress8.py +369 -0
  14. freealg/_algebraic_form/_decompress9.py +363 -0
  15. freealg/_algebraic_form/_decompress_new.py +431 -0
  16. freealg/_algebraic_form/_decompress_new_2.py +1631 -0
  17. freealg/_algebraic_form/_decompress_util.py +172 -0
  18. freealg/_algebraic_form/_edge.py +46 -68
  19. freealg/_algebraic_form/_homotopy.py +62 -30
  20. freealg/_algebraic_form/_homotopy2.py +289 -0
  21. freealg/_algebraic_form/_homotopy3.py +215 -0
  22. freealg/_algebraic_form/_homotopy4.py +320 -0
  23. freealg/_algebraic_form/_homotopy5.py +185 -0
  24. freealg/_algebraic_form/_moments.py +43 -57
  25. freealg/_algebraic_form/_support.py +132 -177
  26. freealg/_algebraic_form/algebraic_form.py +163 -30
  27. freealg/distributions/__init__.py +3 -1
  28. freealg/distributions/_compound_poisson.py +464 -0
  29. freealg/distributions/_deformed_marchenko_pastur.py +51 -0
  30. freealg/distributions/_deformed_wigner.py +44 -0
  31. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/METADATA +2 -1
  32. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/RECORD +36 -20
  33. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/WHEEL +1 -1
  34. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
  35. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
  36. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
freealg/__init__.py CHANGED
@@ -8,13 +8,13 @@
8
8
 
9
9
  from ._free_form import FreeForm, eigvalsh, cond, norm, trace, slogdet, supp, \
10
10
  sample, kde
11
- from ._algebraic_form import AlgebraicForm
11
+ from ._algebraic_form import AlgebraicForm, decompress_newton
12
12
  from ._geometric_form import GeometricForm
13
13
  from . import visualization
14
14
  from . import distributions
15
15
 
16
16
  __all__ = ['FreeForm', 'distributions', 'visualization', 'eigvalsh', 'cond',
17
17
  'norm', 'trace', 'slogdet', 'supp', 'sample', 'kde',
18
- 'AlgebraicForm', 'GeometricForm']
18
+ 'AlgebraicForm', 'GeometricForm', 'decompress_newton']
19
19
 
20
20
  from .__version__ import __version__ # noqa: F401 E402
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.11"
1
+ __version__ = "0.7.14"
@@ -7,5 +7,6 @@
7
7
  # directory of this source tree.
8
8
 
9
9
  from .algebraic_form import AlgebraicForm
10
+ from ._decompress7 import decompress_newton
10
11
 
11
- __all__ = ['AlgebraicForm']
12
+ __all__ = ['AlgebraicForm', 'decompress_newton']
@@ -1,4 +1,3 @@
1
-
2
1
  # SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
3
2
  # SPDX-License-Identifier: BSD-3-Clause
4
3
  # SPDX-FileType: SOURCE
@@ -54,15 +53,61 @@ def _series_pow(mser, j, q_max):
54
53
  # build moment constraints matrix
55
54
  # ===============================
56
55
 
56
+ # def build_moment_constraint_matrix(pairs, deg_z, s, mu):
57
+ #
58
+ # mu = numpy.asarray(mu, dtype=float).ravel()
59
+ # if mu.size == 0:
60
+ # return numpy.zeros((0, len(pairs)), dtype=float)
61
+ #
62
+ # # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
63
+ # r = mu.size - 1
64
+ # q_max = r
65
+ #
66
+ # mser = numpy.zeros(q_max + 1, dtype=float)
67
+ # for p in range(mu.size):
68
+ # q = p + 1
69
+ # if q <= q_max:
70
+ # mser[q] = -float(mu[p])
71
+ #
72
+ # # Precompute (m(t))^j coefficients up to t^{q_max}
73
+ # mpow = []
74
+ # for j in range(s + 1):
75
+ # mpow.append(_series_pow(mser, j, q_max))
76
+ #
77
+ # # Constraints: coeff of t^q in Q(t) := t^{deg_z} P(1/t, m(t)) must be 0
78
+ # # Q(t) = sum_{i,j} c_{i,j} * t^{deg_z - i} * (m(t))^j
79
+ # n_coef = len(pairs)
80
+ # B = numpy.zeros((q_max + 1, n_coef), dtype=float)
81
+ #
82
+ # for k, (i, j) in enumerate(pairs):
83
+ # shift = deg_z - i
84
+ # if shift < 0:
85
+ # continue
86
+ # mj = mpow[j]
87
+ # for q in range(q_max + 1):
88
+ # qq = q - shift
89
+ # if 0 <= qq <= q_max:
90
+ # B[q, k] = mj[qq]
91
+ #
92
+ # # Drop all-zero rows (can happen if index-set can't support higher
93
+ # # moments)
94
+ # row_norm = numpy.linalg.norm(B, axis=1)
95
+ # keep = row_norm > 0.0
96
+ # B = B[keep, :]
97
+ #
98
+ # return B
99
+
57
100
  def build_moment_constraint_matrix(pairs, deg_z, s, mu):
58
101
 
59
102
  mu = numpy.asarray(mu, dtype=float).ravel()
60
103
  if mu.size == 0:
61
104
  return numpy.zeros((0, len(pairs)), dtype=float)
62
105
 
63
- # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
106
+ # mu has entries mu_0..mu_r
64
107
  r = mu.size - 1
65
- q_max = r
108
+
109
+ # Need t^{r+1} in m(t) = -sum mu_p t^{p+1}, otherwise mu_0 is dropped.
110
+ q_max = r + 1
66
111
 
67
112
  mser = numpy.zeros(q_max + 1, dtype=float)
68
113
  for p in range(mu.size):
@@ -70,29 +115,25 @@ def build_moment_constraint_matrix(pairs, deg_z, s, mu):
70
115
  if q <= q_max:
71
116
  mser[q] = -float(mu[p])
72
117
 
73
- # Precompute (m(t))^j coefficients up to t^{q_max}
74
118
  mpow = []
75
119
  for j in range(s + 1):
76
120
  mpow.append(_series_pow(mser, j, q_max))
77
121
 
78
- # Constraints: coeff of t^q in Q(t) := t^{deg_z} P(1/t, m(t)) must be 0
79
- # Q(t) = sum_{i,j} c_{i,j} * t^{deg_z - i} * (m(t))^j
80
122
  n_coef = len(pairs)
81
- B = numpy.zeros((q_max + 1, n_coef), dtype=float)
123
+
124
+ # We only want constraints for l=0..r -> that's q = 0..r in Q(t)
125
+ B = numpy.zeros((r + 1, n_coef), dtype=float)
82
126
 
83
127
  for k, (i, j) in enumerate(pairs):
84
128
  shift = deg_z - i
85
129
  if shift < 0:
86
130
  continue
87
131
  mj = mpow[j]
88
- for q in range(q_max + 1):
132
+ for q in range(r + 1):
89
133
  qq = q - shift
90
134
  if 0 <= qq <= q_max:
91
135
  B[q, k] = mj[qq]
92
136
 
93
- # Drop all-zero rows (can happen if index-set can't support higher moments)
94
137
  row_norm = numpy.linalg.norm(B, axis=1)
95
138
  keep = row_norm > 0.0
96
- B = B[keep, :]
97
-
98
- return B
139
+ return B[keep, :]
@@ -0,0 +1,357 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it under
6
+ # the terms of the license found in the LICENSE.txt file in the root directory
7
+ # of this source tree.
8
+
9
+
10
+ # =======
11
+ # Imports
12
+ # =======
13
+
14
+ import numpy
15
+ import scipy.optimize
16
+ from ._decompress import eval_P_partials
17
+
18
+ __all__ = ["solve_cusp"]
19
+
20
+
21
+ # ==========
22
+ # newton 3x3
23
+ # ==========
24
+
25
+ def _newton_3x3(F, x0, max_iter=60, tol=1e-12, bounds=None, max_step=None):
26
+ x = numpy.array(x0, dtype=float)
27
+
28
+ # bounds: list/tuple of (lo, hi) per component (None means unbounded)
29
+ if bounds is not None:
30
+ b = []
31
+ for lo, hi in bounds:
32
+ b.append((None if lo is None else float(lo),
33
+ None if hi is None else float(hi)))
34
+ bounds = b
35
+
36
+ if max_step is not None:
37
+ max_step = numpy.asarray(max_step, dtype=float)
38
+ if max_step.shape != (3,):
39
+ raise ValueError("max_step must have shape (3,)")
40
+
41
+ def _apply_bounds(xv):
42
+ if bounds is None:
43
+ return xv
44
+ for i, (lo, hi) in enumerate(bounds):
45
+ if lo is not None and xv[i] < lo:
46
+ xv[i] = lo
47
+ if hi is not None and xv[i] > hi:
48
+ xv[i] = hi
49
+ return xv
50
+
51
+ x = _apply_bounds(x.copy())
52
+
53
+ fx = F(x)
54
+ if numpy.linalg.norm(fx) <= tol:
55
+ return x, True, fx
56
+
57
+ for _ in range(max_iter):
58
+ J = numpy.zeros((3, 3), dtype=float)
59
+ eps = 1e-6
60
+ for j in range(3):
61
+ xp = x.copy()
62
+ xp[j] += eps
63
+ xp = _apply_bounds(xp)
64
+ J[:, j] = (F(xp) - fx) / eps
65
+
66
+ try:
67
+ dx = numpy.linalg.solve(J, -fx)
68
+ except numpy.linalg.LinAlgError:
69
+ return x, False, fx
70
+
71
+ if max_step is not None:
72
+ dx = numpy.clip(dx, -max_step, max_step)
73
+
74
+ lam = 1.0
75
+ improved = False
76
+ for _ls in range(12):
77
+ x_try = x + lam * dx
78
+ x_try = _apply_bounds(x_try)
79
+ f_try = F(x_try)
80
+ if numpy.linalg.norm(f_try) < numpy.linalg.norm(fx):
81
+ x, fx = x_try, f_try
82
+ improved = True
83
+ break
84
+ lam *= 0.5
85
+
86
+ if not improved:
87
+ return x, False, fx
88
+
89
+ if numpy.linalg.norm(fx) <= tol:
90
+ return x, True, fx
91
+
92
+ return x, False, fx
93
+
94
+
95
+ __all__ = ["solve_cusp"]
96
+
97
+
98
+ def _second_partials_fd(zeta, y, a_coeffs, eps_z=None, eps_y=None):
99
+ zeta = float(zeta)
100
+ y = float(y)
101
+
102
+ if eps_z is None:
103
+ eps_z = 1e-7 * (1.0 + abs(zeta))
104
+ if eps_y is None:
105
+ eps_y = 1e-7 * (1.0 + abs(y))
106
+
107
+ _, Pz_p, Py_p = eval_P_partials(zeta + eps_z, y, a_coeffs)
108
+ _, Pz_m, Py_m = eval_P_partials(zeta - eps_z, y, a_coeffs)
109
+ Pzz = (Pz_p - Pz_m) / (2.0 * eps_z)
110
+ Pzy1 = (Py_p - Py_m) / (2.0 * eps_z)
111
+
112
+ _, Pz_p, Py_p = eval_P_partials(zeta, y + eps_y, a_coeffs)
113
+ _, Pz_m, Py_m = eval_P_partials(zeta, y - eps_y, a_coeffs)
114
+ Pzy2 = (Pz_p - Pz_m) / (2.0 * eps_y)
115
+ Pyy = (Py_p - Py_m) / (2.0 * eps_y)
116
+
117
+ Pzy = 0.5 * (Pzy1 + Pzy2)
118
+ return float(Pzz), float(Pzy), float(Pyy)
119
+
120
+
121
+ def _cusp_F_real(zeta, y, s, a_coeffs):
122
+ # tau = 1 + exp(s) => c = tau-1 = exp(s) > 0
123
+ c = float(numpy.exp(float(s)))
124
+
125
+ P, Pz, Py = eval_P_partials(float(zeta), float(y), a_coeffs)
126
+ P = float(numpy.real(P))
127
+ Pz = float(numpy.real(Pz))
128
+ Py = float(numpy.real(Py))
129
+
130
+ F1 = P
131
+ F2 = (y * y) * Py - c * Pz
132
+
133
+ Pzz, Pzy, Pyy = _second_partials_fd(zeta, y, a_coeffs)
134
+ F3 = y * (Pzz * (Py * Py) - 2.0 * Pzy * Pz * Py + Pyy * (Pz * Pz)) + \
135
+ 2.0 * (Pz * Pz) * Py
136
+
137
+ return numpy.array([F1, F2, F3], dtype=float)
138
+
139
+
140
+ # ================
141
+ # poly coeffs in y
142
+ # ================
143
+
144
+ def _poly_coeffs_in_y(a_coeffs, zeta):
145
+ a = numpy.asarray(a_coeffs)
146
+ deg_z = a.shape[0] - 1
147
+ deg_y = a.shape[1] - 1
148
+ z_pows = numpy.power(zeta, numpy.arange(deg_z + 1, dtype=numpy.int64))
149
+ c = numpy.empty((deg_y + 1,), dtype=numpy.complex128)
150
+ for j in range(deg_y + 1):
151
+ c[j] = numpy.dot(a[:, j], z_pows)
152
+ return c # ascending in y
153
+
154
+
155
+ # ===================
156
+ # pick realish root y
157
+ # ===================
158
+
159
+ def _pick_realish_root_y(a_coeffs, zeta):
160
+
161
+ c_asc = _poly_coeffs_in_y(a_coeffs, zeta)
162
+ c_desc = c_asc[::-1] # descending for numpy.roots
163
+
164
+ k = 0
165
+ while k < len(c_desc) and abs(c_desc[k]) == 0:
166
+ k += 1
167
+ c_desc = c_desc[k:] if k < len(c_desc) else c_desc
168
+
169
+ if len(c_desc) <= 1:
170
+ return 0.0
171
+
172
+ roots = numpy.roots(c_desc)
173
+ j = int(numpy.argmin(numpy.abs(numpy.imag(roots))))
174
+ return float(numpy.real(roots[j]))
175
+
176
+
177
+ # ==========
178
+ # solve cusp
179
+ # ==========
180
+
181
+ def solve_cusp(
182
+ a_coeffs,
183
+ t_init,
184
+ zeta_init,
185
+ y_init=None,
186
+ max_iter=80,
187
+ tol=1e-12,
188
+ t_bounds=None,
189
+ zeta_bounds=None):
190
+ """
191
+ Exact-derivative cusp solve for (zeta, y, t) with unknowns (zeta, y, s),
192
+ where tau = 1 + exp(s), t = log(tau), x = zeta - (tau-1)/y.
193
+
194
+ a_coeffs: array shape (deg_z+1, deg_y+1), P(zeta,y)=
195
+ sum_{i,j} a[i,j]*zeta^i*y^j
196
+ """
197
+
198
+ a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
199
+ deg_z = a.shape[0] - 1
200
+ deg_y = a.shape[1] - 1
201
+
202
+ def _P_partials_all(zeta, y):
203
+ # returns (P, Pz, Py, Pzz, Pzy, Pyy) as complex
204
+ zeta = numpy.complex128(zeta)
205
+ y = numpy.complex128(y)
206
+
207
+ zi = numpy.power(zeta, numpy.arange(deg_z + 1, dtype=numpy.int64))
208
+ yj = numpy.power(y, numpy.arange(deg_y + 1, dtype=numpy.int64))
209
+
210
+ P = numpy.sum(a * zi[:, None] * yj[None, :])
211
+
212
+ # Pz
213
+ if deg_z >= 1:
214
+ iz = numpy.arange(1, deg_z + 1, dtype=numpy.int64)
215
+ zi_m1 = numpy.power(zeta, iz - 1)
216
+ Pz = numpy.sum(
217
+ (a[iz, :] * iz[:, None]) * zi_m1[:, None] * yj[None, :])
218
+ else:
219
+ Pz = 0.0 + 0.0j
220
+
221
+ # Py
222
+ if deg_y >= 1:
223
+ jy = numpy.arange(1, deg_y + 1, dtype=numpy.int64)
224
+ yj_m1 = numpy.power(y, jy - 1)
225
+ Py = numpy.sum(
226
+ (a[:, jy] * jy[None, :]) * zi[:, None] * yj_m1[None, :])
227
+ else:
228
+ Py = 0.0 + 0.0j
229
+
230
+ # Pzz
231
+ if deg_z >= 2:
232
+ iz = numpy.arange(2, deg_z + 1, dtype=numpy.int64)
233
+ zi_m2 = numpy.power(zeta, iz - 2)
234
+ Pzz = numpy.sum((a[iz, :] * (iz * (iz - 1))[:, None]) *
235
+ zi_m2[:, None] * yj[None, :])
236
+ else:
237
+ Pzz = 0.0 + 0.0j
238
+
239
+ # Pyy
240
+ if deg_y >= 2:
241
+ jy = numpy.arange(2, deg_y + 1, dtype=numpy.int64)
242
+ yj_m2 = numpy.power(y, jy - 2)
243
+ Pyy = numpy.sum((a[:, jy] * (jy * (jy - 1))[None, :]) *
244
+ zi[:, None] * yj_m2[None, :])
245
+ else:
246
+ Pyy = 0.0 + 0.0j
247
+
248
+ # Pzy
249
+ if (deg_z >= 1) and (deg_y >= 1):
250
+ iz = numpy.arange(1, deg_z + 1, dtype=numpy.int64)
251
+ jy = numpy.arange(1, deg_y + 1, dtype=numpy.int64)
252
+ zi_m1 = numpy.power(zeta, iz - 1)
253
+ yj_m1 = numpy.power(y, jy - 1)
254
+ coeff = a[numpy.ix_(iz, jy)] * (iz[:, None] * jy[None, :])
255
+ Pzy = numpy.sum(coeff * zi_m1[:, None] * yj_m1[None, :])
256
+ else:
257
+ Pzy = 0.0 + 0.0j
258
+
259
+ return P, Pz, Py, Pzz, Pzy, Pyy
260
+
261
+ def _F(vec):
262
+ zeta, y, s = float(vec[0]), float(vec[1]), float(vec[2])
263
+ c = float(numpy.exp(s)) # c = tau - 1 > 0
264
+ P, Pz, Py, Pzz, Pzy, Pyy = _P_partials_all(zeta, y)
265
+
266
+ # Work in reals: cusp lives on real zeta,y for real cusp
267
+ P = float(numpy.real(P))
268
+ Pz = float(numpy.real(Pz))
269
+ Py = float(numpy.real(Py))
270
+ Pzz = float(numpy.real(Pzz))
271
+ Pzy = float(numpy.real(Pzy))
272
+ Pyy = float(numpy.real(Pyy))
273
+
274
+ F1 = P
275
+ F2 = (y * y) * Py - c * Pz
276
+ F3 = y * (Pzz * (Py * Py) - 2.0 * Pzy * Pz * Py + Pyy * (Pz * Pz)) + \
277
+ 2.0 * (Pz * Pz) * Py
278
+ return numpy.array([F1, F2, F3], dtype=float)
279
+
280
+ z0 = float(zeta_init)
281
+
282
+ # seed y: keep your provided seed; else pick a real-ish root at z0
283
+ if y_init is None:
284
+ # build polynomial in y at fixed z0 and pick root with smallest imag
285
+ zi = numpy.power(z0, numpy.arange(deg_z + 1, dtype=numpy.int64))
286
+ c_asc = numpy.array([numpy.dot(a[:, j], zi) for j in range(deg_y + 1)],
287
+ dtype=numpy.complex128)
288
+ c_desc = c_asc[::-1]
289
+ kk = 0
290
+ while kk < len(c_desc) and abs(c_desc[kk]) == 0:
291
+ kk += 1
292
+ c_desc = c_desc[kk:] if kk < len(c_desc) else c_desc
293
+ roots = numpy.roots(c_desc) if len(c_desc) > 1 else numpy.array([0.0])
294
+ j = int(numpy.argmin(numpy.abs(numpy.imag(roots))))
295
+ y0 = float(numpy.real(roots[j]))
296
+ else:
297
+ y0 = float(y_init)
298
+
299
+ tau0 = float(numpy.exp(float(t_init)))
300
+ c0 = max(tau0 - 1.0, 1e-14)
301
+ s0 = float(numpy.log(c0))
302
+
303
+ # bounds for zeta, y, s
304
+ z_lo, z_hi = -numpy.inf, numpy.inf
305
+ if zeta_bounds is not None:
306
+ z_lo, z_hi = float(zeta_bounds[0]), float(zeta_bounds[1])
307
+ if z_hi < z_lo:
308
+ z_lo, z_hi = z_hi, z_lo
309
+
310
+ s_lo, s_hi = -numpy.inf, numpy.inf
311
+ if t_bounds is not None:
312
+ t_lo, t_hi = float(t_bounds[0]), float(t_bounds[1])
313
+ if t_hi < t_lo:
314
+ t_lo, t_hi = t_hi, t_lo
315
+ c_lo = max(float(numpy.expm1(t_lo)), 1e-14)
316
+ c_hi = max(float(numpy.expm1(t_hi)), 1e-14)
317
+ s_lo, s_hi = float(numpy.log(c_lo)), float(numpy.log(c_hi))
318
+
319
+ # keep y on the seeded sheet (this is crucial)
320
+ y_rad = 4.0 * (1.0 + abs(y0))
321
+ y_lo, y_hi = float(y0 - y_rad), float(y0 + y_rad)
322
+
323
+ lb = numpy.array([z_lo, y_lo, s_lo], dtype=float)
324
+ ub = numpy.array([z_hi, y_hi, s_hi], dtype=float)
325
+ x0 = numpy.array([z0, y0, s0], dtype=float)
326
+ x0 = numpy.minimum(numpy.maximum(x0, lb), ub)
327
+
328
+ res = scipy.optimize.least_squares(
329
+ _F,
330
+ x0,
331
+ bounds=(lb, ub),
332
+ method="trf",
333
+ max_nfev=int(max_iter) * 100,
334
+ ftol=tol,
335
+ xtol=tol,
336
+ gtol=tol,
337
+ x_scale="jac")
338
+
339
+ zeta, y, s = res.x
340
+ c = float(numpy.exp(float(s)))
341
+ tau = 1.0 + c
342
+ t = float(numpy.log(tau))
343
+ x = float(zeta - (tau - 1.0) / y)
344
+
345
+ F_final = _F(res.x)
346
+ ok = bool(res.success and
347
+ (numpy.max(numpy.abs(F_final)) <= max(1e-9, 50.0 * tol)))
348
+
349
+ return {
350
+ "ok": ok,
351
+ "t": t,
352
+ "tau": float(tau),
353
+ "zeta": float(zeta),
354
+ "y": float(y),
355
+ "x": x,
356
+ "F": F_final,
357
+ "success": bool(res.success)}