freealg 0.1.11__py3-none-any.whl → 0.7.12__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.
- freealg/__init__.py +8 -2
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +12 -0
- freealg/_algebraic_form/_branch_points.py +288 -0
- freealg/_algebraic_form/_constraints.py +139 -0
- freealg/_algebraic_form/_continuation_algebraic.py +706 -0
- freealg/_algebraic_form/_decompress.py +641 -0
- freealg/_algebraic_form/_decompress2.py +204 -0
- freealg/_algebraic_form/_edge.py +330 -0
- freealg/_algebraic_form/_homotopy.py +323 -0
- freealg/_algebraic_form/_moments.py +448 -0
- freealg/_algebraic_form/_sheets_util.py +145 -0
- freealg/_algebraic_form/_support.py +309 -0
- freealg/_algebraic_form/algebraic_form.py +1232 -0
- freealg/_free_form/__init__.py +16 -0
- freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
- freealg/_free_form/_decompress.py +993 -0
- freealg/_free_form/_density_util.py +243 -0
- freealg/_free_form/_jacobi.py +359 -0
- freealg/_free_form/_linalg.py +508 -0
- freealg/{_pade.py → _free_form/_pade.py} +42 -208
- freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
- freealg/{_sample.py → _free_form/_sample.py} +58 -22
- freealg/_free_form/_series.py +454 -0
- freealg/_free_form/_support.py +214 -0
- freealg/_free_form/free_form.py +1362 -0
- freealg/_geometric_form/__init__.py +13 -0
- freealg/_geometric_form/_continuation_genus0.py +175 -0
- freealg/_geometric_form/_continuation_genus1.py +275 -0
- freealg/_geometric_form/_elliptic_functions.py +174 -0
- freealg/_geometric_form/_sphere_maps.py +63 -0
- freealg/_geometric_form/_torus_maps.py +118 -0
- freealg/_geometric_form/geometric_form.py +1094 -0
- freealg/_util.py +56 -110
- freealg/distributions/__init__.py +7 -1
- freealg/distributions/_chiral_block.py +494 -0
- freealg/distributions/_deformed_marchenko_pastur.py +726 -0
- freealg/distributions/_deformed_wigner.py +386 -0
- freealg/distributions/_kesten_mckay.py +29 -15
- freealg/distributions/_marchenko_pastur.py +224 -95
- freealg/distributions/_meixner.py +47 -37
- freealg/distributions/_wachter.py +29 -17
- freealg/distributions/_wigner.py +27 -14
- freealg/visualization/__init__.py +12 -0
- freealg/visualization/_glue_util.py +32 -0
- freealg/visualization/_rgb_hsv.py +125 -0
- freealg-0.7.12.dist-info/METADATA +172 -0
- freealg-0.7.12.dist-info/RECORD +53 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
- freealg/_decompress.py +0 -180
- freealg/_jacobi.py +0 -218
- freealg/_support.py +0 -85
- freealg/freeform.py +0 -967
- freealg-0.1.11.dist-info/METADATA +0 -140
- freealg-0.1.11.dist-info/RECORD +0 -24
- /freealg/{_damp.py → _free_form/_damp.py} +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
from .geometric_form import GeometricForm
|
|
10
|
+
# from ._elliptic_functions import ellipj
|
|
11
|
+
# from ._continuation_genus1 import mobius_z
|
|
12
|
+
|
|
13
|
+
__all__ = ['GeometricForm']
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
|
|
16
|
+
__all__ = ['joukowski_z', 'joukowski_w', 'fit_pade', 'eval_pade',
|
|
17
|
+
'generate_pade']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ===========
|
|
21
|
+
# joukowski z
|
|
22
|
+
# ===========
|
|
23
|
+
|
|
24
|
+
def joukowski_z(w, a, b):
|
|
25
|
+
|
|
26
|
+
c = 0.5 * (a + b)
|
|
27
|
+
d = 0.5 * (b - a)
|
|
28
|
+
z = c + 0.5 * d * (w + 1.0 / w)
|
|
29
|
+
|
|
30
|
+
return z
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ===========
|
|
34
|
+
# joukowski w
|
|
35
|
+
# ===========
|
|
36
|
+
|
|
37
|
+
def joukowski_w(z, a, b):
|
|
38
|
+
|
|
39
|
+
c = 0.5 * (a + b)
|
|
40
|
+
d = 0.5 * (b - a)
|
|
41
|
+
xi = (z - c) / d
|
|
42
|
+
s = numpy.sqrt(xi * xi - 1.0)
|
|
43
|
+
|
|
44
|
+
# Stabilize sqrt branch: make s have same sign as xi (helps continuity)
|
|
45
|
+
s = numpy.where(numpy.real(xi) < 0.0, -s, s)
|
|
46
|
+
|
|
47
|
+
w1 = xi + s
|
|
48
|
+
w2 = xi - s
|
|
49
|
+
w = numpy.where(numpy.abs(w1) >= 1.0, w1, w2)
|
|
50
|
+
|
|
51
|
+
return w
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ========
|
|
55
|
+
# fit pade
|
|
56
|
+
# ========
|
|
57
|
+
|
|
58
|
+
def fit_pade(w, m1, deg_p=12, deg_q=12, ridge_lambda=0.0):
|
|
59
|
+
"""
|
|
60
|
+
Fit m1 on w-plane using Pade rational approximation.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
n_samples = m1.size
|
|
64
|
+
|
|
65
|
+
wp = numpy.ones((n_samples, deg_p + 1), dtype=complex)
|
|
66
|
+
for k in range(1, deg_p + 1):
|
|
67
|
+
wp[:, k] = wp[:, k - 1] * w
|
|
68
|
+
|
|
69
|
+
wq = numpy.ones((n_samples, deg_q + 1), dtype=complex)
|
|
70
|
+
for k in range(1, deg_q + 1):
|
|
71
|
+
wq[:, k] = wq[:, k - 1] * w
|
|
72
|
+
|
|
73
|
+
A = numpy.hstack([wp, -m1[:, None] * wq[:, 1:]])
|
|
74
|
+
bvec = m1
|
|
75
|
+
|
|
76
|
+
# Scale columns for better conditioning on LS
|
|
77
|
+
s = numpy.linalg.norm(A, axis=0)
|
|
78
|
+
s[s == 0] = 1.0
|
|
79
|
+
As = A / s[None, :]
|
|
80
|
+
|
|
81
|
+
if ridge_lambda is None:
|
|
82
|
+
ridge_lambda = 0.0
|
|
83
|
+
|
|
84
|
+
if ridge_lambda > 0.0:
|
|
85
|
+
# Scale ridge by average diagonal magnitude of AhA
|
|
86
|
+
# Since columns of As have unit norm, this is typically ~1.
|
|
87
|
+
alpha = ridge_lambda
|
|
88
|
+
|
|
89
|
+
# Solving augmented least square
|
|
90
|
+
n_coef = As.shape[1]
|
|
91
|
+
A_aug = numpy.vstack([As, numpy.sqrt(alpha) * numpy.eye(
|
|
92
|
+
n_coef, dtype=complex)])
|
|
93
|
+
b_aug = numpy.concatenate([bvec, numpy.zeros(n_coef, dtype=complex)])
|
|
94
|
+
|
|
95
|
+
coef, _, _, _ = numpy.linalg.lstsq(A_aug, b_aug, rcond=None)
|
|
96
|
+
else:
|
|
97
|
+
coef, _, _, _ = numpy.linalg.lstsq(As, bvec, rcond=None)
|
|
98
|
+
|
|
99
|
+
coef = coef / s
|
|
100
|
+
|
|
101
|
+
p = coef[:deg_p + 1]
|
|
102
|
+
q = numpy.zeros(deg_q + 1, dtype=complex)
|
|
103
|
+
q[0] = 1.0
|
|
104
|
+
q[1:] = coef[deg_p + 1:]
|
|
105
|
+
|
|
106
|
+
return p, q
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# =========
|
|
110
|
+
# eval pade
|
|
111
|
+
# =========
|
|
112
|
+
|
|
113
|
+
def eval_pade(w, p, q):
|
|
114
|
+
|
|
115
|
+
num = numpy.zeros_like(w, dtype=complex)
|
|
116
|
+
den = numpy.zeros_like(w, dtype=complex)
|
|
117
|
+
|
|
118
|
+
for k in range(len(p) - 1, -1, -1):
|
|
119
|
+
num = num * w + p[k]
|
|
120
|
+
|
|
121
|
+
for k in range(len(q) - 1, -1, -1):
|
|
122
|
+
den = den * w + q[k]
|
|
123
|
+
|
|
124
|
+
return num / den
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# =============
|
|
128
|
+
# generate pade
|
|
129
|
+
# =============
|
|
130
|
+
|
|
131
|
+
def generate_pade(m1_fn, a, b, deg_p=12, deg_q=12, n_samples=4096, r=1.2,
|
|
132
|
+
n_r=1, r_min=None, ridge_lambda=0.0):
|
|
133
|
+
|
|
134
|
+
if r_min is None:
|
|
135
|
+
r_min = 1.0 + 0.05 * (r - 1.0) if r > 1.0 else 1.0
|
|
136
|
+
|
|
137
|
+
if n_r is None or n_r < 1:
|
|
138
|
+
n_r = 1
|
|
139
|
+
|
|
140
|
+
if n_samples % 2 != 0:
|
|
141
|
+
raise ValueError('n_samples should be even.')
|
|
142
|
+
|
|
143
|
+
if n_r == 1:
|
|
144
|
+
rs = numpy.array([r], dtype=float)
|
|
145
|
+
else:
|
|
146
|
+
rs = numpy.linspace(r_min, r, n_r)
|
|
147
|
+
|
|
148
|
+
W_list = []
|
|
149
|
+
M_list = []
|
|
150
|
+
|
|
151
|
+
n_half = n_samples // 2
|
|
152
|
+
|
|
153
|
+
for r_i in rs:
|
|
154
|
+
|
|
155
|
+
# Generate sample points along theta
|
|
156
|
+
theta = numpy.pi * (numpy.arange(n_half) + 0.5) / n_half
|
|
157
|
+
w = r_i * numpy.exp(1j * theta)
|
|
158
|
+
z = joukowski_z(w, a, b)
|
|
159
|
+
m1 = m1_fn(z)
|
|
160
|
+
|
|
161
|
+
W_list.append(w)
|
|
162
|
+
M_list.append(m1)
|
|
163
|
+
|
|
164
|
+
# Add conjugate points which enforces Schwarz reflection
|
|
165
|
+
W_list.append(numpy.conjugate(w))
|
|
166
|
+
M_list.append(numpy.conjugate(m1))
|
|
167
|
+
|
|
168
|
+
w_all = numpy.concatenate(W_list)
|
|
169
|
+
m1_all = numpy.concatenate(M_list)
|
|
170
|
+
|
|
171
|
+
# Fit on the sample data from m1
|
|
172
|
+
p, q = fit_pade(w_all, m1_all, deg_p=deg_p, deg_q=deg_q,
|
|
173
|
+
ridge_lambda=ridge_lambda)
|
|
174
|
+
|
|
175
|
+
return p, q
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
import scipy
|
|
16
|
+
from ._elliptic_functions import ellipj
|
|
17
|
+
|
|
18
|
+
__all__ = ['mobius_X', 'mobius_z', 'mobius_lambda', 'legendre_Y',
|
|
19
|
+
'fit_rational_xy', '_poly_eval', 'eval_rational_xy',
|
|
20
|
+
'eval_rational_xy_select', 'generate_rational_xy']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =============
|
|
24
|
+
# sqrt pos imag
|
|
25
|
+
# =============
|
|
26
|
+
|
|
27
|
+
def sqrt_pos_imag(z):
|
|
28
|
+
"""
|
|
29
|
+
Square root on a branch cut with always positive imaginary part.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
sq = numpy.sqrt(z)
|
|
33
|
+
sq = numpy.where(sq.imag < 0, -sq, sq)
|
|
34
|
+
|
|
35
|
+
return sq
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ========
|
|
39
|
+
# mobius X
|
|
40
|
+
# ========
|
|
41
|
+
|
|
42
|
+
def mobius_X(z, a1, b1, a2, b2):
|
|
43
|
+
|
|
44
|
+
A = (a2 - b2) / (a2 - a1)
|
|
45
|
+
return A * (z - a1) / (z - b2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ========
|
|
49
|
+
# mobius z
|
|
50
|
+
# ========
|
|
51
|
+
|
|
52
|
+
def mobius_z(X, a1, b1, a2, b2):
|
|
53
|
+
|
|
54
|
+
A = (a2 - b2) / (a2 - a1)
|
|
55
|
+
return (X * b2 - A * a1) / (X - A)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# =============
|
|
59
|
+
# mobius lambda
|
|
60
|
+
# =============
|
|
61
|
+
|
|
62
|
+
def mobius_lambda(a1, b1, a2, b2):
|
|
63
|
+
|
|
64
|
+
num = (b1 - a1) * (b2 - a2)
|
|
65
|
+
den = (a2 - a1) * (b2 - b1)
|
|
66
|
+
|
|
67
|
+
return num / den
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ==========
|
|
71
|
+
# legendre Y
|
|
72
|
+
# ==========
|
|
73
|
+
|
|
74
|
+
def legendre_Y(X, lam):
|
|
75
|
+
|
|
76
|
+
D = X * (1.0 - X) * (X - lam)
|
|
77
|
+
Y = sqrt_pos_imag(D)
|
|
78
|
+
|
|
79
|
+
flip = numpy.real(X) > 1.0
|
|
80
|
+
Y = numpy.where(flip, -Y, Y)
|
|
81
|
+
|
|
82
|
+
return Y
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ===============
|
|
86
|
+
# fit rational xy
|
|
87
|
+
# ===============
|
|
88
|
+
|
|
89
|
+
def fit_rational_xy(X, Y, F, deg_p0=12, deg_p1=12, deg_q=12, ridge_lambda=0.0,
|
|
90
|
+
weights=None):
|
|
91
|
+
|
|
92
|
+
n = F.size
|
|
93
|
+
|
|
94
|
+
max_deg = max(deg_p0, deg_p1, deg_q)
|
|
95
|
+
xp = numpy.ones((n, max_deg + 1), dtype=complex)
|
|
96
|
+
for k in range(1, max_deg + 1):
|
|
97
|
+
xp[:, k] = xp[:, k - 1] * X
|
|
98
|
+
|
|
99
|
+
A0 = xp[:, :deg_p0 + 1]
|
|
100
|
+
A1 = (Y[:, None] * xp[:, :deg_p1 + 1])
|
|
101
|
+
Aq = (-F[:, None] * xp[:, 1:deg_q + 1])
|
|
102
|
+
|
|
103
|
+
A = numpy.hstack([A0, A1, Aq])
|
|
104
|
+
bvec = F
|
|
105
|
+
|
|
106
|
+
if weights is not None:
|
|
107
|
+
w = numpy.sqrt(numpy.maximum(weights, 0.0))
|
|
108
|
+
A = A * w[:, None]
|
|
109
|
+
bvec = bvec * w
|
|
110
|
+
|
|
111
|
+
s = numpy.linalg.norm(A, axis=0)
|
|
112
|
+
s[s == 0] = 1.0
|
|
113
|
+
As = A / s[None, :]
|
|
114
|
+
|
|
115
|
+
if ridge_lambda is None:
|
|
116
|
+
ridge_lambda = 0.0
|
|
117
|
+
|
|
118
|
+
if ridge_lambda > 0.0:
|
|
119
|
+
alpha = ridge_lambda
|
|
120
|
+
n_coef = As.shape[1]
|
|
121
|
+
A_aug = numpy.vstack(
|
|
122
|
+
[As, numpy.sqrt(alpha) * numpy.eye(n_coef, dtype=complex)])
|
|
123
|
+
b_aug = numpy.concatenate([bvec, numpy.zeros(n_coef, dtype=complex)])
|
|
124
|
+
coef, _, _, _ = numpy.linalg.lstsq(A_aug, b_aug, rcond=None)
|
|
125
|
+
else:
|
|
126
|
+
coef, _, _, _ = numpy.linalg.lstsq(As, bvec, rcond=None)
|
|
127
|
+
|
|
128
|
+
coef = coef / s
|
|
129
|
+
|
|
130
|
+
i0 = deg_p0 + 1
|
|
131
|
+
i1 = i0 + (deg_p1 + 1)
|
|
132
|
+
|
|
133
|
+
p0 = coef[:i0]
|
|
134
|
+
p1 = coef[i0:i1]
|
|
135
|
+
|
|
136
|
+
q = numpy.zeros(deg_q + 1, dtype=complex)
|
|
137
|
+
q[0] = 1.0
|
|
138
|
+
q[1:] = coef[i1:]
|
|
139
|
+
|
|
140
|
+
return p0, p1, q
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# =========
|
|
144
|
+
# poly eval
|
|
145
|
+
# =========
|
|
146
|
+
|
|
147
|
+
def _poly_eval(x, c):
|
|
148
|
+
|
|
149
|
+
y = numpy.zeros_like(x, dtype=complex)
|
|
150
|
+
for k in range(len(c) - 1, -1, -1):
|
|
151
|
+
y = y * x + c[k]
|
|
152
|
+
|
|
153
|
+
return y
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ================
|
|
157
|
+
# eval rational xy
|
|
158
|
+
# ================
|
|
159
|
+
|
|
160
|
+
def eval_rational_xy(X, Y, p0, p1, q, alt_branch):
|
|
161
|
+
num0 = _poly_eval(X, p0)
|
|
162
|
+
num1 = _poly_eval(X, p1)
|
|
163
|
+
den = _poly_eval(X, q)
|
|
164
|
+
|
|
165
|
+
m_plus = (num0 + Y * num1) / den
|
|
166
|
+
m_minus = (num0 - Y * num1) / den
|
|
167
|
+
|
|
168
|
+
if alt_branch:
|
|
169
|
+
return m_minus
|
|
170
|
+
return m_plus
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# =======================
|
|
174
|
+
# eval rational xy select
|
|
175
|
+
# =======================
|
|
176
|
+
|
|
177
|
+
def eval_rational_xy_select(z, X, Y, p0, p1, q, alt_branch):
|
|
178
|
+
|
|
179
|
+
m_plus = eval_rational_xy(X, Y, p0, p1, q, alt_branch=False)
|
|
180
|
+
m_minus = eval_rational_xy(X, Y, p0, p1, q, alt_branch=True)
|
|
181
|
+
|
|
182
|
+
mask_p = numpy.imag(z) >= 0.0
|
|
183
|
+
|
|
184
|
+
pick_plus_p = (numpy.imag(m_plus) >= 0.0)
|
|
185
|
+
pick_plus_m = (numpy.imag(m_plus) <= 0.0)
|
|
186
|
+
|
|
187
|
+
pick_plus = numpy.where(mask_p, pick_plus_p, pick_plus_m)
|
|
188
|
+
|
|
189
|
+
m1 = numpy.where(pick_plus, m_plus, m_minus)
|
|
190
|
+
m2 = numpy.where(pick_plus, m_minus, m_plus)
|
|
191
|
+
|
|
192
|
+
if alt_branch:
|
|
193
|
+
return m2
|
|
194
|
+
return m1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ====================
|
|
198
|
+
# generate rational xy
|
|
199
|
+
# ====================
|
|
200
|
+
|
|
201
|
+
def generate_rational_xy(m1_fn, a1, b1, a2, b2, deg_p0=12, deg_p1=12, deg_q=12,
|
|
202
|
+
n_samples=4096, r=0.25, n_r=1, r_min=None,
|
|
203
|
+
ridge_lambda=0.0):
|
|
204
|
+
|
|
205
|
+
if n_samples % 2 != 0:
|
|
206
|
+
raise ValueError('n_samples should be even.')
|
|
207
|
+
|
|
208
|
+
if n_r is None or n_r < 1:
|
|
209
|
+
n_r = 1
|
|
210
|
+
|
|
211
|
+
lam = mobius_lambda(a1, b1, a2, b2)
|
|
212
|
+
|
|
213
|
+
r = float(r)
|
|
214
|
+
if r > 0.6:
|
|
215
|
+
r = 0.30
|
|
216
|
+
|
|
217
|
+
if r_min is None:
|
|
218
|
+
r_min = 0.25 * r
|
|
219
|
+
|
|
220
|
+
if n_r == 1:
|
|
221
|
+
vs = numpy.array([r], dtype=float)
|
|
222
|
+
else:
|
|
223
|
+
vs = numpy.linspace(r_min, r, n_r)
|
|
224
|
+
|
|
225
|
+
K = float(scipy.special.ellipk(lam))
|
|
226
|
+
Kp = float(scipy.special.ellipk(1.0 - lam))
|
|
227
|
+
|
|
228
|
+
omega1 = 2.0 * K
|
|
229
|
+
shift2 = 1j * Kp
|
|
230
|
+
|
|
231
|
+
n_half = n_samples // 2
|
|
232
|
+
t = (numpy.arange(n_half) + 0.5) / n_half
|
|
233
|
+
u_re = omega1 * t
|
|
234
|
+
|
|
235
|
+
X_list = []
|
|
236
|
+
Y_list = []
|
|
237
|
+
F_list = []
|
|
238
|
+
|
|
239
|
+
for v in vs:
|
|
240
|
+
for sh in (0.0, shift2):
|
|
241
|
+
|
|
242
|
+
u = u_re + sh + 1j * v
|
|
243
|
+
|
|
244
|
+
sn, cn, dn, _ = ellipj(u, lam)
|
|
245
|
+
|
|
246
|
+
X = lam * (sn * sn)
|
|
247
|
+
|
|
248
|
+
Y = legendre_Y(X, lam)
|
|
249
|
+
Y_ref = 1j * lam * (sn * cn * dn)
|
|
250
|
+
if sh != 0.0:
|
|
251
|
+
Y_ref = -Y_ref
|
|
252
|
+
|
|
253
|
+
flip = numpy.real(Y * numpy.conjugate(Y_ref)) < 0.0
|
|
254
|
+
Y = numpy.where(flip, -Y, Y)
|
|
255
|
+
|
|
256
|
+
z = mobius_z(X, a1, b1, a2, b2)
|
|
257
|
+
f = m1_fn(z)
|
|
258
|
+
|
|
259
|
+
X_list.append(X)
|
|
260
|
+
Y_list.append(Y)
|
|
261
|
+
F_list.append(f)
|
|
262
|
+
|
|
263
|
+
X_list.append(numpy.conjugate(X))
|
|
264
|
+
Y_list.append(numpy.conjugate(Y))
|
|
265
|
+
F_list.append(numpy.conjugate(f))
|
|
266
|
+
|
|
267
|
+
X_all = numpy.concatenate(X_list)
|
|
268
|
+
Y_all = numpy.concatenate(Y_list)
|
|
269
|
+
f_all = numpy.concatenate(F_list)
|
|
270
|
+
|
|
271
|
+
p0, p1, q = fit_rational_xy(X_all, Y_all, f_all, deg_p0=deg_p0,
|
|
272
|
+
deg_p1=deg_p1, deg_q=deg_q,
|
|
273
|
+
ridge_lambda=ridge_lambda)
|
|
274
|
+
|
|
275
|
+
return p0, p1, q, lam
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
The Jacobi elliptic functions sn, cn, and dn are implemented in:
|
|
12
|
+
|
|
13
|
+
1. scipy.special: but they do not accept complex inputs
|
|
14
|
+
2. mpmath: but they do not have vectorization (very slow)
|
|
15
|
+
|
|
16
|
+
Because of these, we implemented sn, cn, and dn ourselves. There is no need to
|
|
17
|
+
implement the elliptic K function, since scipy.special already has it (and
|
|
18
|
+
vectorized) and we only need that for real inputs, not complex inputs. So, no
|
|
19
|
+
need to re-implement the scipy.special.ellipk function.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =======
|
|
24
|
+
# Imports
|
|
25
|
+
# =======
|
|
26
|
+
|
|
27
|
+
import numpy
|
|
28
|
+
from scipy import special
|
|
29
|
+
|
|
30
|
+
__all__ = ['ellipj']
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ===========
|
|
34
|
+
# jacobi nome
|
|
35
|
+
# ===========
|
|
36
|
+
|
|
37
|
+
def _jacobi_nome(m):
|
|
38
|
+
"""
|
|
39
|
+
Compute the Jacobi nome q = exp(-pi K(1-m)/K(m)) and the complete elliptic
|
|
40
|
+
integrals K(m), K(1-m).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
m = float(m)
|
|
44
|
+
K = special.ellipk(m)
|
|
45
|
+
Kp = special.ellipk(1.0 - m)
|
|
46
|
+
q = numpy.exp(-numpy.pi * Kp / K)
|
|
47
|
+
|
|
48
|
+
return q, K, Kp
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# =======
|
|
52
|
+
# theta 1
|
|
53
|
+
# =======
|
|
54
|
+
|
|
55
|
+
def _theta1(v, q, n_terms):
|
|
56
|
+
"""
|
|
57
|
+
Jacobi theta_1(v,q) via truncated Fourier series (vectorized for complex
|
|
58
|
+
v).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
v = numpy.asarray(v, dtype=complex)
|
|
62
|
+
n = numpy.arange(n_terms, dtype=float)
|
|
63
|
+
a = 2.0 * n + 1.0
|
|
64
|
+
logq = numpy.log(q)
|
|
65
|
+
w = numpy.exp(((n + 0.5) ** 2) * logq) * ((-1.0) ** n)
|
|
66
|
+
s = numpy.sin(a[:, None] * v[None, :])
|
|
67
|
+
|
|
68
|
+
return 2.0 * (w[:, None] * s).sum(axis=0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =======
|
|
72
|
+
# theta 2
|
|
73
|
+
# =======
|
|
74
|
+
|
|
75
|
+
def _theta2(v, q, n_terms):
|
|
76
|
+
"""
|
|
77
|
+
Jacobi theta_2(v,q) via truncated Fourier series (vectorized for complex
|
|
78
|
+
v).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
v = numpy.asarray(v, dtype=complex)
|
|
82
|
+
n = numpy.arange(n_terms, dtype=float)
|
|
83
|
+
a = 2.0 * n + 1.0
|
|
84
|
+
logq = numpy.log(q)
|
|
85
|
+
w = numpy.exp(((n + 0.5) ** 2) * logq)
|
|
86
|
+
c = numpy.cos(a[:, None] * v[None, :])
|
|
87
|
+
|
|
88
|
+
return 2.0 * (w[:, None] * c).sum(axis=0)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# =======
|
|
92
|
+
# theta 3
|
|
93
|
+
# =======
|
|
94
|
+
|
|
95
|
+
def _theta3(v, q, n_terms):
|
|
96
|
+
"""
|
|
97
|
+
Jacobi theta_3(v,q) via truncated Fourier series (vectorized for complex
|
|
98
|
+
v).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
v = numpy.asarray(v, dtype=complex)
|
|
102
|
+
n = numpy.arange(1, n_terms + 1, dtype=float)
|
|
103
|
+
logq = numpy.log(q)
|
|
104
|
+
w = numpy.exp((n ** 2) * logq)
|
|
105
|
+
c = numpy.cos((2.0 * n)[:, None] * v[None, :])
|
|
106
|
+
|
|
107
|
+
return 1.0 + 2.0 * (w[:, None] * c).sum(axis=0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =======
|
|
111
|
+
# theta 4
|
|
112
|
+
# =======
|
|
113
|
+
|
|
114
|
+
def _theta4(v, q, n_terms):
|
|
115
|
+
"""
|
|
116
|
+
Jacobi theta_4(v,q) via truncated Fourier series (vectorized for complex
|
|
117
|
+
v).
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
v = numpy.asarray(v, dtype=complex)
|
|
121
|
+
n = numpy.arange(1, n_terms + 1, dtype=float)
|
|
122
|
+
logq = numpy.log(q)
|
|
123
|
+
w = numpy.exp((n ** 2) * logq) * ((-1.0) ** n)
|
|
124
|
+
c = numpy.cos((2.0 * n)[:, None] * v[None, :])
|
|
125
|
+
|
|
126
|
+
return 1.0 + 2.0 * (w[:, None] * c).sum(axis=0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ======
|
|
130
|
+
# ellipj
|
|
131
|
+
# ======
|
|
132
|
+
|
|
133
|
+
def ellipj(u, m, n_terms=None):
|
|
134
|
+
"""
|
|
135
|
+
Vectorized Jacobi elliptic sn, cn, dn for complex u and real m in (0,1),
|
|
136
|
+
computed from theta functions.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
u = numpy.asarray(u, dtype=complex)
|
|
140
|
+
q, K, Kp = _jacobi_nome(m)
|
|
141
|
+
|
|
142
|
+
if n_terms is None:
|
|
143
|
+
if q < 1e-6:
|
|
144
|
+
n_terms = 16
|
|
145
|
+
elif q < 1e-3:
|
|
146
|
+
n_terms = 24
|
|
147
|
+
elif q < 1e-2:
|
|
148
|
+
n_terms = 32
|
|
149
|
+
elif q < 5e-2:
|
|
150
|
+
n_terms = 48
|
|
151
|
+
else:
|
|
152
|
+
n_terms = 80
|
|
153
|
+
|
|
154
|
+
v = (numpy.pi / (2.0 * K)) * u
|
|
155
|
+
v_flat = v.ravel()
|
|
156
|
+
|
|
157
|
+
th1 = _theta1(v_flat, q, n_terms)
|
|
158
|
+
th2 = _theta2(v_flat, q, n_terms)
|
|
159
|
+
th3 = _theta3(v_flat, q, n_terms)
|
|
160
|
+
th4 = _theta4(v_flat, q, n_terms)
|
|
161
|
+
|
|
162
|
+
th2_0 = _theta2(numpy.array([0.0], dtype=complex), q, n_terms)[0]
|
|
163
|
+
th3_0 = _theta3(numpy.array([0.0], dtype=complex), q, n_terms)[0]
|
|
164
|
+
th4_0 = _theta4(numpy.array([0.0], dtype=complex), q, n_terms)[0]
|
|
165
|
+
|
|
166
|
+
sn = (th3_0 * th1) / (th2_0 * th4)
|
|
167
|
+
cn = (th4_0 * th2) / (th2_0 * th4)
|
|
168
|
+
dn = (th4_0 * th3) / (th3_0 * th4)
|
|
169
|
+
|
|
170
|
+
sn = sn.reshape(v.shape)
|
|
171
|
+
cn = cn.reshape(v.shape)
|
|
172
|
+
dn = dn.reshape(v.shape)
|
|
173
|
+
|
|
174
|
+
return sn, cn, dn, q
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
from ._continuation_genus0 import joukowski_z
|
|
16
|
+
|
|
17
|
+
__all__ = ['make_sphere_grid', 'eval_on_sphere']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ===========================
|
|
21
|
+
# stereographic w from sphere
|
|
22
|
+
# ===========================
|
|
23
|
+
|
|
24
|
+
def stereographic_w_from_sphere(X, Y, Z):
|
|
25
|
+
|
|
26
|
+
den = 1.0 - Z
|
|
27
|
+
w = (X + 1j * Y) / den
|
|
28
|
+
return w
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ================
|
|
32
|
+
# make sphere grid
|
|
33
|
+
# ================
|
|
34
|
+
|
|
35
|
+
def make_sphere_grid(n_theta=100, n_phi=50):
|
|
36
|
+
|
|
37
|
+
theta = numpy.linspace(0.0, 2.0 * numpy.pi, n_theta, endpoint=False)
|
|
38
|
+
u = numpy.linspace(0.0, 1.0, n_phi)
|
|
39
|
+
phi = numpy.pi * (u * u)
|
|
40
|
+
TH, PH = numpy.meshgrid(theta, phi)
|
|
41
|
+
X = numpy.sin(PH) * numpy.cos(TH)
|
|
42
|
+
Y = numpy.sin(PH) * numpy.sin(TH)
|
|
43
|
+
Z = numpy.cos(PH)
|
|
44
|
+
|
|
45
|
+
return X, Y, Z
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ==============
|
|
49
|
+
# eval on sphere
|
|
50
|
+
# ==============
|
|
51
|
+
|
|
52
|
+
def eval_on_sphere(X, Y, Z, a, b, m1_fn, m2_fn, z_eps=1e-9):
|
|
53
|
+
|
|
54
|
+
Zc = numpy.minimum(Z, 1.0 - 1e-12)
|
|
55
|
+
w = stereographic_w_from_sphere(X, Y, Zc)
|
|
56
|
+
z = joukowski_z(w, a, b)
|
|
57
|
+
z = z + 1j * z_eps
|
|
58
|
+
m1 = m1_fn(z)
|
|
59
|
+
m2 = m2_fn(z)
|
|
60
|
+
out = numpy.array(m2, copy=True)
|
|
61
|
+
out[numpy.abs(w) <= 1.0] = m1[numpy.abs(w) <= 1.0]
|
|
62
|
+
|
|
63
|
+
return out
|