freealg 0.6.2__py3-none-any.whl → 0.7.0__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 (44) hide show
  1. freealg/__init__.py +8 -7
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +11 -0
  4. freealg/_algebraic_form/_continuation_algebraic.py +503 -0
  5. freealg/_algebraic_form/_decompress.py +648 -0
  6. freealg/_algebraic_form/_edge.py +352 -0
  7. freealg/_algebraic_form/_sheets_util.py +145 -0
  8. freealg/_algebraic_form/algebraic_form.py +987 -0
  9. freealg/_freeform/__init__.py +16 -0
  10. freealg/{_decompress.py → _freeform/_decompress.py} +0 -10
  11. freealg/_freeform/_density_util.py +243 -0
  12. freealg/{_linalg.py → _freeform/_linalg.py} +1 -1
  13. freealg/{_pade.py → _freeform/_pade.py} +0 -1
  14. freealg/{freeform.py → _freeform/freeform.py} +2 -31
  15. freealg/_geometric_form/__init__.py +13 -0
  16. freealg/_geometric_form/_continuation_genus0.py +175 -0
  17. freealg/_geometric_form/_continuation_genus1.py +275 -0
  18. freealg/_geometric_form/_elliptic_functions.py +174 -0
  19. freealg/_geometric_form/_sphere_maps.py +63 -0
  20. freealg/_geometric_form/_torus_maps.py +118 -0
  21. freealg/_geometric_form/geometric_form.py +1094 -0
  22. freealg/_util.py +1 -217
  23. freealg/distributions/__init__.py +5 -1
  24. freealg/distributions/_chiral_block.py +440 -0
  25. freealg/distributions/_deformed_marchenko_pastur.py +617 -0
  26. freealg/distributions/_deformed_wigner.py +312 -0
  27. freealg/distributions/_marchenko_pastur.py +197 -80
  28. freealg/visualization/__init__.py +12 -0
  29. freealg/visualization/_glue_util.py +32 -0
  30. freealg/visualization/_rgb_hsv.py +125 -0
  31. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/METADATA +9 -11
  32. freealg-0.7.0.dist-info/RECORD +47 -0
  33. freealg-0.6.2.dist-info/RECORD +0 -26
  34. /freealg/{_chebyshev.py → _freeform/_chebyshev.py} +0 -0
  35. /freealg/{_damp.py → _freeform/_damp.py} +0 -0
  36. /freealg/{_jacobi.py → _freeform/_jacobi.py} +0 -0
  37. /freealg/{_plot_util.py → _freeform/_plot_util.py} +0 -0
  38. /freealg/{_sample.py → _freeform/_sample.py} +0 -0
  39. /freealg/{_series.py → _freeform/_series.py} +0 -0
  40. /freealg/{_support.py → _freeform/_support.py} +0 -0
  41. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/WHEEL +0 -0
  42. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/licenses/AUTHORS.txt +0 -0
  43. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/licenses/LICENSE.txt +0 -0
  44. {freealg-0.6.2.dist-info → freealg-0.7.0.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