freealg 0.6.3__py3-none-any.whl → 0.7.1__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 -7
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +11 -0
- freealg/_algebraic_form/_continuation_algebraic.py +503 -0
- freealg/_algebraic_form/_decompress.py +648 -0
- freealg/_algebraic_form/_edge.py +352 -0
- freealg/_algebraic_form/_sheets_util.py +145 -0
- freealg/_algebraic_form/algebraic_form.py +987 -0
- freealg/_freeform/__init__.py +16 -0
- freealg/_freeform/_density_util.py +243 -0
- freealg/{_linalg.py → _freeform/_linalg.py} +1 -1
- freealg/{freeform.py → _freeform/freeform.py} +2 -1
- 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 +1 -228
- freealg/distributions/__init__.py +5 -1
- freealg/distributions/_chiral_block.py +440 -0
- freealg/distributions/_deformed_marchenko_pastur.py +617 -0
- freealg/distributions/_deformed_wigner.py +312 -0
- freealg/distributions/_kesten_mckay.py +2 -2
- freealg/distributions/_marchenko_pastur.py +199 -82
- freealg/distributions/_meixner.py +2 -2
- freealg/distributions/_wachter.py +2 -2
- freealg/distributions/_wigner.py +2 -2
- freealg/visualization/__init__.py +12 -0
- freealg/visualization/_glue_util.py +32 -0
- freealg/visualization/_rgb_hsv.py +125 -0
- {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/METADATA +1 -1
- freealg-0.7.1.dist-info/RECORD +47 -0
- freealg-0.6.3.dist-info/RECORD +0 -26
- /freealg/{_chebyshev.py → _freeform/_chebyshev.py} +0 -0
- /freealg/{_damp.py → _freeform/_damp.py} +0 -0
- /freealg/{_decompress.py → _freeform/_decompress.py} +0 -0
- /freealg/{_jacobi.py → _freeform/_jacobi.py} +0 -0
- /freealg/{_pade.py → _freeform/_pade.py} +0 -0
- /freealg/{_plot_util.py → _freeform/_plot_util.py} +0 -0
- /freealg/{_sample.py → _freeform/_sample.py} +0 -0
- /freealg/{_series.py → _freeform/_series.py} +0 -0
- /freealg/{_support.py → _freeform/_support.py} +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/WHEEL +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
from ._continuation_genus1 import _poly_eval
|
|
18
|
+
|
|
19
|
+
__all__ = ['make_torus_grid', 'u_from_angles', 'eval_fitted_m_on_torus']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ===============
|
|
23
|
+
# make torus grid
|
|
24
|
+
# ===============
|
|
25
|
+
|
|
26
|
+
def make_torus_grid(n_theta=101, n_phi=101, R=1.0, r=0.35):
|
|
27
|
+
"""
|
|
28
|
+
Returns an embedded torus mesh (X,Y,Z) with angles (TH,PH).
|
|
29
|
+
TH: around major circle, PH: around tube.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
if n_phi % 2 == 0:
|
|
33
|
+
raise ValueError('n_phi should be odd number to avoid rendering ' +
|
|
34
|
+
'issue at phi=0.')
|
|
35
|
+
|
|
36
|
+
# fundamental angles (no endpoint duplicates)
|
|
37
|
+
theta = numpy.linspace(0.0, 2.0*numpy.pi, int(n_theta), endpoint=False)
|
|
38
|
+
phi = numpy.linspace(0.0, 2.0*numpy.pi, int(n_phi), endpoint=False)
|
|
39
|
+
|
|
40
|
+
TH, PH = numpy.meshgrid(theta, phi) # shapes (n_phi, n_theta)
|
|
41
|
+
|
|
42
|
+
# --- wrap/close the grid by appending first row/col ---
|
|
43
|
+
TH = numpy.vstack([TH, TH[0:1, :]]) # add phi seam row
|
|
44
|
+
PH = numpy.vstack([PH, PH[0:1, :]])
|
|
45
|
+
|
|
46
|
+
TH = numpy.hstack([TH, TH[:, 0:1]]) # add theta seam col
|
|
47
|
+
PH = numpy.hstack([PH, PH[:, 0:1]])
|
|
48
|
+
|
|
49
|
+
# torus embedding
|
|
50
|
+
X = (R + r*numpy.cos(PH)) * numpy.cos(TH)
|
|
51
|
+
Y = (R + r*numpy.cos(PH)) * numpy.sin(TH)
|
|
52
|
+
Z = r * numpy.sin(PH)
|
|
53
|
+
|
|
54
|
+
return X, Y, Z, TH, PH
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============
|
|
58
|
+
# u from angles
|
|
59
|
+
# =============
|
|
60
|
+
|
|
61
|
+
def u_from_angles(TH, PH, lam, center=(0.0, 0.0)):
|
|
62
|
+
"""
|
|
63
|
+
Map angles (TH,PH) in [0,2pi)^2 to the elliptic u-plane fundamental cell.
|
|
64
|
+
|
|
65
|
+
u = u0 + (omega1/2pi)*TH + (omega2/2pi)*PH,
|
|
66
|
+
omega1 = 2K(m), omega2 = 2 i K(1-m).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
m = float(lam)
|
|
70
|
+
K = scipy.special.ellipk(m)
|
|
71
|
+
Kp = scipy.special.ellipk(1.0 - m)
|
|
72
|
+
omega1 = 2.0 * K
|
|
73
|
+
omega2 = 2.0j * Kp
|
|
74
|
+
|
|
75
|
+
u0 = complex(center[0], center[1]) # shift inside the fundamental domain
|
|
76
|
+
u = u0 + (omega1/(2.0*numpy.pi))*TH + (omega2/(2.0*numpy.pi))*PH
|
|
77
|
+
|
|
78
|
+
return u
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ==========================
|
|
82
|
+
# evaluate fitted m on torus
|
|
83
|
+
# ==========================
|
|
84
|
+
|
|
85
|
+
def eval_fitted_m_on_torus(u, a1, b1, a2, b2, p0, p1, q, lam):
|
|
86
|
+
"""
|
|
87
|
+
Evaluate the *uniformized* branch m(u) = (p0(X)+Y p1(X))/q(X)
|
|
88
|
+
with X = lam * sn(u)^2 and Y chosen to match the elliptic derivative sign.
|
|
89
|
+
|
|
90
|
+
Requires jacobi_ellipj(u, m) that supports complex arrays and returns
|
|
91
|
+
(sn,cn,dn).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
sn, cn, dn, _ = ellipj(u, lam)
|
|
95
|
+
|
|
96
|
+
X = lam * (sn * sn)
|
|
97
|
+
|
|
98
|
+
# canonical algebraic Y from curve: Y^2 = X(1-X)(X-lam)
|
|
99
|
+
D = X * (1.0 - X) * (X - lam)
|
|
100
|
+
|
|
101
|
+
Y = numpy.sqrt(D)
|
|
102
|
+
|
|
103
|
+
# enforce "positive imag" convention first
|
|
104
|
+
Y = numpy.where(numpy.imag(Y) < 0.0, -Y, Y)
|
|
105
|
+
|
|
106
|
+
# now align sign with elliptic reference:
|
|
107
|
+
# for Legendre normalization: dX/du = 2 i Y_ref, where Y_ref is equal to
|
|
108
|
+
# i*lam*sn*cn*dn (up to consistent conventions)
|
|
109
|
+
Y_ref = 1j * lam * (sn * cn * dn)
|
|
110
|
+
flip = numpy.real(Y * numpy.conjugate(Y_ref)) < 0.0
|
|
111
|
+
Y = numpy.where(flip, -Y, Y)
|
|
112
|
+
|
|
113
|
+
num0 = _poly_eval(X, p0)
|
|
114
|
+
num1 = _poly_eval(X, p1)
|
|
115
|
+
den = _poly_eval(X, q)
|
|
116
|
+
|
|
117
|
+
m = (num0 + Y * num1) / den
|
|
118
|
+
return m
|