freealg 0.7.11__tar.gz → 0.7.14__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.
- {freealg-0.7.11 → freealg-0.7.14}/MANIFEST.in +0 -1
- {freealg-0.7.11 → freealg-0.7.14}/PKG-INFO +2 -1
- {freealg-0.7.11 → freealg-0.7.14}/freealg/__init__.py +2 -2
- freealg-0.7.14/freealg/__version__.py +1 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/__init__.py +2 -1
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_constraints.py +53 -12
- freealg-0.7.14/freealg/_algebraic_form/_cusp.py +357 -0
- freealg-0.7.14/freealg/_algebraic_form/_cusp_wrap.py +268 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress.py +641 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress2.py +206 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress4.py +739 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress5.py +738 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress6.py +492 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress7.py +355 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress8.py +369 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress9.py +363 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress_new.py +431 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress_new_2.py +1631 -0
- freealg-0.7.14/freealg/_algebraic_form/_decompress_util.py +172 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_edge.py +46 -68
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_homotopy.py +62 -30
- freealg-0.7.14/freealg/_algebraic_form/_homotopy2.py +289 -0
- freealg-0.7.14/freealg/_algebraic_form/_homotopy3.py +215 -0
- freealg-0.7.14/freealg/_algebraic_form/_homotopy4.py +320 -0
- freealg-0.7.14/freealg/_algebraic_form/_homotopy5.py +185 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_moments.py +43 -57
- freealg-0.7.14/freealg/_algebraic_form/_support.py +264 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/algebraic_form.py +163 -30
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/__init__.py +3 -1
- freealg-0.7.14/freealg/distributions/_compound_poisson.py +464 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_deformed_marchenko_pastur.py +51 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_deformed_wigner.py +44 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/PKG-INFO +2 -1
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/SOURCES.txt +16 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/requires.txt +1 -0
- {freealg-0.7.11 → freealg-0.7.14}/requirements.txt +2 -1
- freealg-0.7.11/freealg/__version__.py +0 -1
- freealg-0.7.11/freealg/_algebraic_form/_decompress.py +0 -692
- freealg-0.7.11/freealg/_algebraic_form/_decompress2.py +0 -86
- freealg-0.7.11/freealg/_algebraic_form/_support.py +0 -309
- {freealg-0.7.11 → freealg-0.7.14}/AUTHORS.txt +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/CHANGELOG.rst +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/LICENSE.txt +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/README.rst +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_branch_points.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_continuation_algebraic.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_algebraic_form/_sheets_util.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/__init__.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_chebyshev.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_damp.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_decompress.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_density_util.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_jacobi.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_linalg.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_pade.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_plot_util.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_sample.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_series.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/_support.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_free_form/free_form.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/__init__.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/_continuation_genus0.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/_continuation_genus1.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/_elliptic_functions.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/_sphere_maps.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/_torus_maps.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_geometric_form/geometric_form.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/_util.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_chiral_block.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_kesten_mckay.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_marchenko_pastur.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_meixner.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_wachter.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/distributions/_wigner.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/visualization/__init__.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/visualization/_glue_util.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg/visualization/_rgb_hsv.py +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/dependency_links.txt +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/not-zip-safe +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/freealg.egg-info/top_level.txt +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/pyproject.toml +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/setup.cfg +0 -0
- {freealg-0.7.11 → freealg-0.7.14}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.14
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Home-page: https://github.com/ameli/freealg
|
|
6
6
|
Download-URL: https://github.com/ameli/freealg/archive/main.zip
|
|
@@ -37,6 +37,7 @@ Requires-Dist: matplotlib
|
|
|
37
37
|
Requires-Dist: colorcet
|
|
38
38
|
Requires-Dist: statsmodels
|
|
39
39
|
Requires-Dist: numba
|
|
40
|
+
Requires-Dist: tqdm
|
|
40
41
|
Provides-Extra: test
|
|
41
42
|
Requires-Dist: tox; extra == "test"
|
|
42
43
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.14"
|
|
@@ -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
|
-
#
|
|
106
|
+
# mu has entries mu_0..mu_r
|
|
64
107
|
r = mu.size - 1
|
|
65
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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)}
|