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,993 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# SPDX-FileType: SOURCE
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
6
|
+
# of this source tree.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# =======
|
|
10
|
+
# Imports
|
|
11
|
+
# =======
|
|
12
|
+
|
|
13
|
+
import numpy
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import texplot
|
|
16
|
+
|
|
17
|
+
# Fallback to previous numpy API
|
|
18
|
+
if not hasattr(numpy, 'trapezoid'):
|
|
19
|
+
numpy.trapezoid = numpy.trapz
|
|
20
|
+
|
|
21
|
+
__all__ = ['decompress', 'reverse_characteristics']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ==========
|
|
25
|
+
# derivative
|
|
26
|
+
# ==========
|
|
27
|
+
|
|
28
|
+
def _derivative(f, fz, z, h, fd_order=4, vertical=False, return_second=False):
|
|
29
|
+
"""
|
|
30
|
+
Compute first or first and second derivatives.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
|
|
35
|
+
f : function
|
|
36
|
+
Calling function
|
|
37
|
+
|
|
38
|
+
fz : numpy.array
|
|
39
|
+
Function values precomputed at the array z. This is used for second
|
|
40
|
+
order derivative where the stencil also contains the middle point.
|
|
41
|
+
Since this is already computed outside the function call, it is reused
|
|
42
|
+
here, essentially making second-order derivative free.
|
|
43
|
+
|
|
44
|
+
z : numpy.array
|
|
45
|
+
A 1D array of complex points to evaluate derivative at.
|
|
46
|
+
|
|
47
|
+
h : float or numpy.array
|
|
48
|
+
Stencil size
|
|
49
|
+
|
|
50
|
+
fd_order : {2, 4, 6}, default=2
|
|
51
|
+
Order of central finite differencing scheme.
|
|
52
|
+
|
|
53
|
+
vertical : bool, default=False
|
|
54
|
+
It `True`, it uses vertical stencil (along y axis) instead of
|
|
55
|
+
horizontal (along x axis).
|
|
56
|
+
|
|
57
|
+
return_second : bool, default=False
|
|
58
|
+
If `True`, returns both first and second derivatives.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
|
|
63
|
+
df1 : numpy.array
|
|
64
|
+
First derivative
|
|
65
|
+
|
|
66
|
+
If ``return_second=True``:
|
|
67
|
+
|
|
68
|
+
df2 : numpy.array
|
|
69
|
+
Second derivative
|
|
70
|
+
|
|
71
|
+
Notes
|
|
72
|
+
-----
|
|
73
|
+
|
|
74
|
+
Uses central finite difference.
|
|
75
|
+
|
|
76
|
+
If the function is holomorphic, taking the derivative along horizontal or
|
|
77
|
+
vertical directions should be identical (in theory), however, in practice,
|
|
78
|
+
they might not be exactly the same. Note especially that ``vertical=True``
|
|
79
|
+
is not suitable for points close to branch cut where the stencil points
|
|
80
|
+
fall into two branches.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
h = numpy.asarray(h, dtype=z.dtype)
|
|
84
|
+
|
|
85
|
+
if vertical:
|
|
86
|
+
h_ = 1j * h
|
|
87
|
+
else:
|
|
88
|
+
h_ = h
|
|
89
|
+
|
|
90
|
+
# Stencil indices
|
|
91
|
+
stencil = numpy.arange(-fd_order//2, fd_order//2 + 1).astype(float)
|
|
92
|
+
|
|
93
|
+
# Stencil coefficients
|
|
94
|
+
if fd_order == 2:
|
|
95
|
+
coeff_df1 = [-0.5, +0.0, +0.5]
|
|
96
|
+
coeff_df2 = [+1.0, -2.0, +1.0]
|
|
97
|
+
|
|
98
|
+
elif fd_order == 4:
|
|
99
|
+
coeff_df1 = [+1.0 / 12.0, -2.0 / 3.0, +0.0, +2.0 / 3.0, -1.0 / 12.0]
|
|
100
|
+
coeff_df2 = [-1.0 / 12.0, +16.0 / 12.0, -30.0 / 12.0, +16.0 / 12.0,
|
|
101
|
+
-1.0 / 12.0]
|
|
102
|
+
|
|
103
|
+
elif fd_order == 6:
|
|
104
|
+
coeff_df1 = [-1.0 / 60.0, +3.0 / 20.0, -3.0 / 4.0, +0.0, +3.0 / 4.0,
|
|
105
|
+
-3.0 / 20.0, +1.0 / 60.0]
|
|
106
|
+
coeff_df2 = [+1.0 / 90, -3.0 / 20.0, +3.0 / 2.0, -49.0 / 18.0,
|
|
107
|
+
+3.0 / 2.0, -3.0 / 20.0, +1.0 / 90.0]
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
raise NotImplementedError('"fd_order" is not valid.')
|
|
111
|
+
|
|
112
|
+
# Function values at stencil points. Precomputed to avoid redundancy when
|
|
113
|
+
# both first and second derivatives are needed.
|
|
114
|
+
val = [None] * stencil.size
|
|
115
|
+
for i in range(stencil.size):
|
|
116
|
+
|
|
117
|
+
# The center stencil for first derivative is zero, so we do not compute
|
|
118
|
+
# the function value, unless second derivative is needed.
|
|
119
|
+
if coeff_df1[i] == 0.0:
|
|
120
|
+
# This is already computed outside this function, for free. This
|
|
121
|
+
# is only used for second derivative where the stencil has non-zero
|
|
122
|
+
# coefficient in the middle point. This is not used in the first
|
|
123
|
+
# derivative where the coefficient of the middle point is zero.
|
|
124
|
+
val[i] = fz
|
|
125
|
+
else:
|
|
126
|
+
val[i] = f(z + stencil[i] * h_)
|
|
127
|
+
|
|
128
|
+
# First derivative
|
|
129
|
+
df1 = numpy.zeros_like(z)
|
|
130
|
+
for i in range(stencil.size):
|
|
131
|
+
|
|
132
|
+
# Skip the center of stencil where the coeff is zero.
|
|
133
|
+
if coeff_df1[i] != +0.0:
|
|
134
|
+
df1 += coeff_df1[i] * val[i]
|
|
135
|
+
df1 /= h_
|
|
136
|
+
|
|
137
|
+
# Second derivative
|
|
138
|
+
if return_second:
|
|
139
|
+
df2 = numpy.zeros_like(z)
|
|
140
|
+
for i in range(stencil.size):
|
|
141
|
+
df2 += coeff_df2[i] * val[i]
|
|
142
|
+
df2 /= h_**2
|
|
143
|
+
|
|
144
|
+
if return_second:
|
|
145
|
+
return df1, df2
|
|
146
|
+
else:
|
|
147
|
+
return df1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ===================
|
|
151
|
+
# adaptive derivative
|
|
152
|
+
# ===================
|
|
153
|
+
|
|
154
|
+
def _adaptive_derivative(f, fz, z, h, err_low=1e-4, err_high=1e-2, factor=2.0,
|
|
155
|
+
vertical=False, return_second=False):
|
|
156
|
+
"""
|
|
157
|
+
Compute first or first and second derivatives using adaptive refinement of
|
|
158
|
+
stencil size.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
|
|
163
|
+
f : function
|
|
164
|
+
Calling function
|
|
165
|
+
|
|
166
|
+
fz : numpy.array
|
|
167
|
+
Function values precomputed at the array z. This is used for second
|
|
168
|
+
order derivative where the stencil also contains the middle point.
|
|
169
|
+
Since this is already computed outside the function call, it is reused
|
|
170
|
+
here, essentially making second-order derivative free.
|
|
171
|
+
|
|
172
|
+
z : numpy.array
|
|
173
|
+
A 1D array of complex points to evaluate derivative at.
|
|
174
|
+
|
|
175
|
+
h : float or numpy.array
|
|
176
|
+
Stencil size
|
|
177
|
+
|
|
178
|
+
err_low : float, default=1e-4
|
|
179
|
+
Threshold criteria to increase stencil size h. Namely, the relative
|
|
180
|
+
error of Richardson test below this threshold is considered as
|
|
181
|
+
being caused by having too small stencil size, and hence stencil size
|
|
182
|
+
h has to be increased.
|
|
183
|
+
|
|
184
|
+
err_high : float, default=1e-2
|
|
185
|
+
Threshold criteria to decrease stencil size h. Namely, the relative
|
|
186
|
+
error of Richardson test above this threshold is considered as
|
|
187
|
+
being caused by having too large stencil size, and hence stencil size
|
|
188
|
+
h has to be decreased.
|
|
189
|
+
|
|
190
|
+
factor : float, default=2.0
|
|
191
|
+
Factor to increase or decrease stencil size.
|
|
192
|
+
|
|
193
|
+
vertical : bool, default=False
|
|
194
|
+
It `True`, it uses vertical stencil (along y axis) instead of
|
|
195
|
+
horizontal (along x axis).
|
|
196
|
+
|
|
197
|
+
return_second : bool, default=False
|
|
198
|
+
If `True`, returns both first and second derivatives.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
|
|
203
|
+
h : numpy.array
|
|
204
|
+
Updated stencil size
|
|
205
|
+
|
|
206
|
+
df1 : numpy.array
|
|
207
|
+
First derivative
|
|
208
|
+
|
|
209
|
+
If ``return_second=True``:
|
|
210
|
+
|
|
211
|
+
df2 : numpy.array
|
|
212
|
+
Second derivative
|
|
213
|
+
|
|
214
|
+
Notes
|
|
215
|
+
-----
|
|
216
|
+
|
|
217
|
+
Uses central finite difference. This function uses finite difference of
|
|
218
|
+
order 2 or 4 as follows.
|
|
219
|
+
|
|
220
|
+
First, function f is evaluated on 5 points:
|
|
221
|
+
|
|
222
|
+
[f(z-2h), f(z-h), f(z), f(z+h), f(z+2h)]
|
|
223
|
+
|
|
224
|
+
This is used for either a:
|
|
225
|
+
|
|
226
|
+
1. 2-nd order finite difference with the stencil of size h
|
|
227
|
+
2. 2-nd order finite difference with the stencil of size 2h
|
|
228
|
+
3. 4-th order finite difference with the stencil of size h
|
|
229
|
+
|
|
230
|
+
To determine which of these should be used, the first derivative for the
|
|
231
|
+
cases 1 and 2 above are compared using the Richardson test:
|
|
232
|
+
|
|
233
|
+
1. If the Richardson test determines the stencil is too small, the output
|
|
234
|
+
derivative is a second order finite difference on stencil of size 2h.
|
|
235
|
+
Also, h is grown by factor of two for the next iteration.
|
|
236
|
+
|
|
237
|
+
2. If the Richardson test determines the stencil is good (not too small or
|
|
238
|
+
not too large), the output derivative is a fourth order finite
|
|
239
|
+
difference on stencil of size h. Also, h is not changed for the next
|
|
240
|
+
iteration.
|
|
241
|
+
|
|
242
|
+
3. If the Richardson test determines the stencil is too large, the output
|
|
243
|
+
derivative is a second order finite difference on stencil of size h.
|
|
244
|
+
Also, h is shrunken by factor of two for the next iteration.
|
|
245
|
+
|
|
246
|
+
If the function is holomorphic, taking the derivative along horizontal or
|
|
247
|
+
vertical directions should be identical (in theory), however, in practice,
|
|
248
|
+
they might not be exactly the same. Note especially that ``vertical=True``
|
|
249
|
+
is not suitable for points close to branch cut where the stencil points
|
|
250
|
+
fall into two branches.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
h = h.copy().astype(z.dtype)
|
|
254
|
+
|
|
255
|
+
if vertical:
|
|
256
|
+
h_ = 1j * h
|
|
257
|
+
else:
|
|
258
|
+
h_ = h
|
|
259
|
+
|
|
260
|
+
# Center index of stencil of order 4
|
|
261
|
+
st_center = 2
|
|
262
|
+
|
|
263
|
+
# Stencil indices of order 2 of size h
|
|
264
|
+
st_ord2_h = numpy.array([-1, 0, 1]) + st_center
|
|
265
|
+
|
|
266
|
+
# Stencil indices of order 2 of size 2h
|
|
267
|
+
st_ord2_2h = numpy.array([-2, 0, 2]) + st_center
|
|
268
|
+
|
|
269
|
+
# Stencil indices of order 4 of size h
|
|
270
|
+
st_ord4_h = numpy.array([-2, -1, 0, 1, 2]) + st_center
|
|
271
|
+
|
|
272
|
+
# Stencil coefficients for first derivative, order 2
|
|
273
|
+
coeff_df1_ord2 = [-0.5, +0.0, +0.5]
|
|
274
|
+
|
|
275
|
+
# Stencil coefficients for second derivative, order 2
|
|
276
|
+
coeff_df2_ord2 = [+1.0, -2.0, +1.0]
|
|
277
|
+
|
|
278
|
+
# Stencil coefficients for first derivative, order 4
|
|
279
|
+
coeff_df1_ord4 = [+1.0 / 12.0, -2.0 / 3.0, 0.0, +2.0 / 3.0, -1.0 / 12.0]
|
|
280
|
+
|
|
281
|
+
# Stencil coefficients for second derivative, order 4
|
|
282
|
+
coeff_df2_ord4 = [-1.0 / 12.0, +16.0 / 12.0, -30.0 / 12.0, +16.0 / 12.0,
|
|
283
|
+
-1.0 / 12.0]
|
|
284
|
+
|
|
285
|
+
# Function values at stencil points of order 4. Precomputed to avoid
|
|
286
|
+
# redundancy when both first and second derivatives are needed.
|
|
287
|
+
f_val = [None] * st_ord4_h.size
|
|
288
|
+
for i in range(st_ord4_h.size):
|
|
289
|
+
|
|
290
|
+
if coeff_df1_ord4[i] == 0.0:
|
|
291
|
+
# This is already computed outside this function, for free. This
|
|
292
|
+
# is only used for second derivative where the stencil has non-zero
|
|
293
|
+
# coefficient in the middle point. This is not used in the first
|
|
294
|
+
# derivative where the coefficient of the middle point is zero.
|
|
295
|
+
f_val[i] = fz
|
|
296
|
+
else:
|
|
297
|
+
f_val[i] = f(z + st_ord4_h[i] * h_)
|
|
298
|
+
|
|
299
|
+
# First derivative, fd order 2, stencil of size h
|
|
300
|
+
df1_ord2_h = numpy.zeros_like(z)
|
|
301
|
+
for i in range(st_ord2_h.size):
|
|
302
|
+
# Skip the center of stencil where the coeff is zero.
|
|
303
|
+
if coeff_df1_ord2[i] != 0.0:
|
|
304
|
+
df1_ord2_h += coeff_df1_ord2[i] * f_val[st_ord2_h[i]]
|
|
305
|
+
df1_ord2_h /= h_
|
|
306
|
+
|
|
307
|
+
# First derivative, fd order 2, stencil of size 2h
|
|
308
|
+
df1_ord2_2h = numpy.zeros_like(z)
|
|
309
|
+
for i in range(st_ord2_2h.size):
|
|
310
|
+
# Skip the center of stencil where the coeff is zero.
|
|
311
|
+
if coeff_df1_ord2[i] != 0.0:
|
|
312
|
+
df1_ord2_2h += coeff_df1_ord2[i] * f_val[st_ord2_2h[i]]
|
|
313
|
+
df1_ord2_2h /= (2.0 * h_)
|
|
314
|
+
|
|
315
|
+
# First derivative, fd order 4, stencil of size h
|
|
316
|
+
df1_ord4_h = numpy.zeros_like(z)
|
|
317
|
+
for i in range(st_ord4_h.size):
|
|
318
|
+
# Skip the center of stencil where the coeff is zero.
|
|
319
|
+
if coeff_df1_ord4[i] != 0.0:
|
|
320
|
+
df1_ord4_h += coeff_df1_ord4[i] * f_val[st_ord4_h[i]]
|
|
321
|
+
df1_ord4_h /= h_
|
|
322
|
+
|
|
323
|
+
if return_second:
|
|
324
|
+
# Second derivative, fd order 2, stencil of size h
|
|
325
|
+
df2_ord2_h = numpy.zeros_like(z)
|
|
326
|
+
for i in range(st_ord2_h.size):
|
|
327
|
+
df2_ord2_h += coeff_df2_ord2[i] * f_val[st_ord2_h[i]]
|
|
328
|
+
df2_ord2_h /= h_**2
|
|
329
|
+
|
|
330
|
+
# Second derivative, fd order 2, stencil of size 2h
|
|
331
|
+
df2_ord2_2h = numpy.zeros_like(z)
|
|
332
|
+
for i in range(st_ord2_2h.size):
|
|
333
|
+
df2_ord2_2h += coeff_df2_ord2[i] * f_val[st_ord2_2h[i]]
|
|
334
|
+
df2_ord2_2h /= (2.0 * h_)**2
|
|
335
|
+
|
|
336
|
+
# Second derivative, fd order 4, stencil of size h
|
|
337
|
+
df2_ord4_h = numpy.zeros_like(z)
|
|
338
|
+
for i in range(st_ord4_h.size):
|
|
339
|
+
df2_ord4_h += coeff_df2_ord4[i] * f_val[st_ord4_h[i]]
|
|
340
|
+
df2_ord4_h /= h_**2
|
|
341
|
+
|
|
342
|
+
# Richardson test
|
|
343
|
+
fd_order = 2
|
|
344
|
+
p = 2**fd_order - 1.0
|
|
345
|
+
abs_err = numpy.abs(df1_ord2_2h - df1_ord2_h) / p
|
|
346
|
+
rel_err = abs_err / numpy.maximum(1.0, numpy.abs(df1_ord2_h))
|
|
347
|
+
|
|
348
|
+
# Grow and Shrink limits criteria on relative error
|
|
349
|
+
grow = rel_err < err_low
|
|
350
|
+
shrink = rel_err > err_high
|
|
351
|
+
|
|
352
|
+
# Output stencil size
|
|
353
|
+
h[grow] *= factor
|
|
354
|
+
h[shrink] /= factor
|
|
355
|
+
|
|
356
|
+
# Output first derivative
|
|
357
|
+
df1 = df1_ord4_h
|
|
358
|
+
df1[grow] = df1_ord2_2h[grow]
|
|
359
|
+
df1[shrink] = df1_ord2_h[shrink]
|
|
360
|
+
|
|
361
|
+
# Output second derivative
|
|
362
|
+
if return_second:
|
|
363
|
+
df2 = df2_ord4_h
|
|
364
|
+
df2[grow] = df2_ord2_2h[grow]
|
|
365
|
+
df2[shrink] = df2_ord2_h[shrink]
|
|
366
|
+
|
|
367
|
+
if return_second:
|
|
368
|
+
return h, df1, df2
|
|
369
|
+
else:
|
|
370
|
+
return h, df1
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# =============
|
|
374
|
+
# newton method
|
|
375
|
+
# =============
|
|
376
|
+
|
|
377
|
+
def _newton_method(f, z_init, a, support, enforce_wall=False, tol=1e-4,
|
|
378
|
+
step_size=0.1, max_iter=500, adaptive_stencil=True,
|
|
379
|
+
halley=False, vertical=False):
|
|
380
|
+
"""
|
|
381
|
+
Solves :math:``f(z) = a`` for many starting points simultaneously using the
|
|
382
|
+
damped Newton method.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
|
|
387
|
+
f : function
|
|
388
|
+
Caller function.
|
|
389
|
+
|
|
390
|
+
z_init : numpy.array
|
|
391
|
+
Initial guess of roots
|
|
392
|
+
|
|
393
|
+
a : numpy.array
|
|
394
|
+
Target value of f in the root finding problem f(z) - a = 0.
|
|
395
|
+
|
|
396
|
+
support : tuple
|
|
397
|
+
The support of the density. This is needed to enforce no crossing
|
|
398
|
+
through branch cut as wall.
|
|
399
|
+
|
|
400
|
+
enforce_wall : bool, default=False
|
|
401
|
+
If `True`, roots are not allowed to cross branch cut. It is recommended
|
|
402
|
+
to enable this feature when the initial guess z_init is above the real
|
|
403
|
+
line. When z_init points are below the real line, this feature is
|
|
404
|
+
effectively not used.
|
|
405
|
+
|
|
406
|
+
tol : float, default=1e-4
|
|
407
|
+
Tolerance of terminating the iteration when |f(z) - a| < tol.
|
|
408
|
+
|
|
409
|
+
step_size : float, default=0.1
|
|
410
|
+
The step size of Newton (or Halley) iterations, between 0 and 1 where
|
|
411
|
+
1 corresponds to full Newton (or Halley) step. Note that these methods
|
|
412
|
+
are generally very sensitive to the step size. Values between 0.05 and
|
|
413
|
+
0.2 are recommended.
|
|
414
|
+
|
|
415
|
+
max_ier : int, default=500
|
|
416
|
+
Maximum number of iterations.
|
|
417
|
+
|
|
418
|
+
adaptive_stencil : bool, default=True
|
|
419
|
+
If `True`, the stencil size in finite differencing is adaptively
|
|
420
|
+
selected based on Richardson extrapolation error. If `False`, a fixed
|
|
421
|
+
stencil size of all points are used.
|
|
422
|
+
|
|
423
|
+
halley : bool, default=False
|
|
424
|
+
If `True`, Halley method is used instead of Newton method, but only
|
|
425
|
+
when the Newton makes very small increments. Halley method is in
|
|
426
|
+
general not suitable for the most of the trajectory, and can only be
|
|
427
|
+
useful at the end of trajectory for convergence. It is recommended to
|
|
428
|
+
turn this off.
|
|
429
|
+
|
|
430
|
+
vertical : bool, default=False
|
|
431
|
+
If `True`, to compute the derivative of holomorphic function, a
|
|
432
|
+
vertical stencil (instead of horizontal) is used. This is not suitable
|
|
433
|
+
for points near branch cut where some points of the stencil might fall
|
|
434
|
+
intro two sides of the branch cut, but it might be suitable for points
|
|
435
|
+
inside the support.
|
|
436
|
+
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
|
|
440
|
+
roots: numpy.array
|
|
441
|
+
Solutions z of f(z) - a = 0.
|
|
442
|
+
|
|
443
|
+
residuals: numpy.array
|
|
444
|
+
The residuals f(z) - a
|
|
445
|
+
|
|
446
|
+
iterations : numpy.array
|
|
447
|
+
Number of iterations used for each root.
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
# Finite difference order
|
|
451
|
+
fd_order = 4
|
|
452
|
+
|
|
453
|
+
epsilon = numpy.sqrt(numpy.finfo(float).eps)
|
|
454
|
+
h_base = epsilon**(1.0 / (fd_order + 1.0))
|
|
455
|
+
|
|
456
|
+
# Global variables
|
|
457
|
+
root = z_init.copy()
|
|
458
|
+
mask = numpy.ones(z_init.shape, dtype=bool)
|
|
459
|
+
f_val = numpy.zeros(z_init.shape, dtype=z_init.dtype)
|
|
460
|
+
residual = numpy.ones(z_init.shape, dtype=z_init.dtype) * numpy.inf
|
|
461
|
+
iterations = numpy.zeros(z_init.shape, dtype=int)
|
|
462
|
+
|
|
463
|
+
# Initialize stencil size
|
|
464
|
+
h = h_base * numpy.maximum(1.0, numpy.abs(z_init))
|
|
465
|
+
|
|
466
|
+
# Main loop
|
|
467
|
+
for _ in range(max_iter):
|
|
468
|
+
|
|
469
|
+
if not numpy.any(mask):
|
|
470
|
+
# No more active point left
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
# Update iterations
|
|
474
|
+
iterations += mask.astype(int)
|
|
475
|
+
|
|
476
|
+
# Mask variables using the previous mask (dash m are masked variables)
|
|
477
|
+
a_m = a[mask]
|
|
478
|
+
z_m = root[mask]
|
|
479
|
+
f_m = f(z_m)
|
|
480
|
+
f_val[mask] = f_m
|
|
481
|
+
residual_m = f_m - a_m
|
|
482
|
+
residual[mask] = residual_m
|
|
483
|
+
|
|
484
|
+
# Update mask
|
|
485
|
+
mask = numpy.abs(residual) >= tol
|
|
486
|
+
|
|
487
|
+
# Mask variables again using the new mask
|
|
488
|
+
a_m = a[mask]
|
|
489
|
+
z_m = root[mask]
|
|
490
|
+
f_m = f_val[mask]
|
|
491
|
+
residual_m = residual[mask]
|
|
492
|
+
h_m = h[mask]
|
|
493
|
+
|
|
494
|
+
if adaptive_stencil:
|
|
495
|
+
# Use adaptive stencil size from previous update
|
|
496
|
+
h_m = h[mask]
|
|
497
|
+
|
|
498
|
+
# Adaptive stencil size, finite difference order is fixed 2 and 4.
|
|
499
|
+
h_m, df1_m, df2_m = _adaptive_derivative(f, f_m, z_m, h_m,
|
|
500
|
+
vertical=vertical,
|
|
501
|
+
return_second=True)
|
|
502
|
+
|
|
503
|
+
# Clip too small or too large stencils
|
|
504
|
+
h_m = numpy.clip(h_m, epsilon, h_base * numpy.abs(z_m))
|
|
505
|
+
|
|
506
|
+
# Update global stencil sizes
|
|
507
|
+
h[mask] = h_m
|
|
508
|
+
|
|
509
|
+
else:
|
|
510
|
+
# Fixed stencil size for all points
|
|
511
|
+
h_m = h_base * numpy.maximum(1.0, numpy.abs(z_m))
|
|
512
|
+
|
|
513
|
+
# Fixed stencil size, but finite difference order can be set.
|
|
514
|
+
fd_order = 4 # can be 2, 4, 6
|
|
515
|
+
df1_m, df2_m = _derivative(f, f_m, z_m, h_m, fd_order=fd_order,
|
|
516
|
+
vertical=False, return_second=True)
|
|
517
|
+
|
|
518
|
+
# Handling second order zeros
|
|
519
|
+
df1_m[numpy.abs(df1_m) < 1e-12 * numpy.abs(z_m)] = 1e-12
|
|
520
|
+
|
|
521
|
+
# Newton and Halley steps
|
|
522
|
+
df0_m = residual_m
|
|
523
|
+
newton_step = df0_m / df1_m
|
|
524
|
+
|
|
525
|
+
if halley:
|
|
526
|
+
halley_step = (df0_m * df1_m) / (df1_m**2 - 0.5 * df0_m * df2_m)
|
|
527
|
+
|
|
528
|
+
# Criteria on where to use Halley instead of Newton
|
|
529
|
+
use_halley = numpy.abs(newton_step) < 1e-2 * numpy.abs(z_m)
|
|
530
|
+
step = numpy.where(use_halley, halley_step, newton_step)
|
|
531
|
+
else:
|
|
532
|
+
step = newton_step
|
|
533
|
+
|
|
534
|
+
# Force new point to not cross branch cut. It has to go from upper to
|
|
535
|
+
# lower half plane only through the support interval.
|
|
536
|
+
if enforce_wall:
|
|
537
|
+
|
|
538
|
+
root_m_old = root[mask]
|
|
539
|
+
root_m_new = z_m - step_size * step
|
|
540
|
+
lam_m, lam_p = support
|
|
541
|
+
|
|
542
|
+
x0, y0 = root_m_old.real, root_m_old.imag
|
|
543
|
+
x1, y1 = root_m_new.real, root_m_new.imag
|
|
544
|
+
|
|
545
|
+
# Find which points are crossing the branch cut (either downward
|
|
546
|
+
# or upward)
|
|
547
|
+
crossed = \
|
|
548
|
+
(y0 * y1 <= 0.0) & \
|
|
549
|
+
((x0 <= lam_m) | (x0 >= lam_p)) & \
|
|
550
|
+
((x1 <= lam_m) | (x1 >= lam_p))
|
|
551
|
+
|
|
552
|
+
# Remove imaginary component from step
|
|
553
|
+
step[crossed] = step[crossed].real + 0.0j
|
|
554
|
+
|
|
555
|
+
# Update root
|
|
556
|
+
root[mask] = z_m - step_size * step
|
|
557
|
+
|
|
558
|
+
# Residuals at the final points
|
|
559
|
+
residual = f(root) - a
|
|
560
|
+
|
|
561
|
+
return root, residual, iterations
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# =============
|
|
565
|
+
# secant method
|
|
566
|
+
# =============
|
|
567
|
+
|
|
568
|
+
def _secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
|
|
569
|
+
alpha=0.5, max_bt=1, eps=1e-30, step_factor=5.0,
|
|
570
|
+
post_smooth=True, jump_tol=10.0, dtype=numpy.complex128,
|
|
571
|
+
verbose=False):
|
|
572
|
+
"""
|
|
573
|
+
Solves :math:``f(z) = a`` for many starting points simultaneously using the
|
|
574
|
+
secant method in the complex plane.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
f : callable
|
|
579
|
+
Function that accepts and returns complex `ndarray`s.
|
|
580
|
+
|
|
581
|
+
z0, z1 : array_like
|
|
582
|
+
Two initial guesses. ``z1`` may be broadcast to ``z0``.
|
|
583
|
+
|
|
584
|
+
a : complex or array_like, optional
|
|
585
|
+
Right-hand-side targets (broadcasted to ``z0``). Defaults to ``0+0j``.
|
|
586
|
+
|
|
587
|
+
tol : float, optional
|
|
588
|
+
Convergence criterion on ``|f(z) - a|``. Defaults to ``1e-12``.
|
|
589
|
+
|
|
590
|
+
max_iter : int, optional
|
|
591
|
+
Maximum number of secant iterations. Defaults to ``100``.
|
|
592
|
+
|
|
593
|
+
alpha : float, optional
|
|
594
|
+
Back-tracking shrink factor (``0 < alpha < 1``). Defaults to ``0.5``.
|
|
595
|
+
|
|
596
|
+
max_bt : int, optional
|
|
597
|
+
Maximum back-tracking trials per iteration. Defaults to ``0``.
|
|
598
|
+
|
|
599
|
+
eps : float, optional
|
|
600
|
+
Safeguard added to tiny denominators. Defaults to ``1e-30``.
|
|
601
|
+
|
|
602
|
+
post_smooth : bool, optional
|
|
603
|
+
If True (default) run a single vectorised clean-up pass that
|
|
604
|
+
re-solves points whose final root differs from the *nearest*
|
|
605
|
+
neighbour by more than ``jump_tol`` times the local median jump.
|
|
606
|
+
|
|
607
|
+
jump_tol : float, optional
|
|
608
|
+
Sensitivity of the clean-up pass; larger tolerance implies fewer
|
|
609
|
+
re-solves.
|
|
610
|
+
|
|
611
|
+
dtype : {``'complex128'``, ``'complex256'``}, default = ``'complex128'``
|
|
612
|
+
Data type for inner computations of complex variables.
|
|
613
|
+
|
|
614
|
+
verbose : bool, optional
|
|
615
|
+
If *True*, prints progress every 10 iterations.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
roots : ndarray
|
|
620
|
+
Estimated roots, shaped like the broadcast inputs.
|
|
621
|
+
residuals : ndarray
|
|
622
|
+
Final residuals ``|f(root) - a|``.
|
|
623
|
+
iterations : ndarray
|
|
624
|
+
Iteration count for each point.
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
# Broadcast inputs
|
|
628
|
+
z0, z1, a = numpy.broadcast_arrays(
|
|
629
|
+
numpy.asarray(z0, dtype=dtype),
|
|
630
|
+
numpy.asarray(z1, dtype=dtype),
|
|
631
|
+
numpy.asarray(a, dtype=dtype),
|
|
632
|
+
)
|
|
633
|
+
orig_shape = z0.shape
|
|
634
|
+
z0, z1, a = (x.ravel() for x in (z0, z1, a))
|
|
635
|
+
|
|
636
|
+
n_points = z0.size
|
|
637
|
+
roots = z1.copy()
|
|
638
|
+
iterations = numpy.zeros(n_points, dtype=int)
|
|
639
|
+
|
|
640
|
+
f0 = f(z0) - a
|
|
641
|
+
f1 = f(z1) - a
|
|
642
|
+
residuals = numpy.abs(f1)
|
|
643
|
+
converged = residuals < tol
|
|
644
|
+
|
|
645
|
+
# Entering main loop
|
|
646
|
+
for k in range(max_iter):
|
|
647
|
+
active = ~converged
|
|
648
|
+
if not active.any():
|
|
649
|
+
break
|
|
650
|
+
|
|
651
|
+
# Secant step
|
|
652
|
+
denom = f1 - f0
|
|
653
|
+
denom = numpy.where(numpy.abs(denom) < eps, denom + eps, denom)
|
|
654
|
+
dz = (z1 - z0) * f1 / denom
|
|
655
|
+
|
|
656
|
+
# Step-size limiter
|
|
657
|
+
prev_step = numpy.maximum(numpy.abs(z1 - z0), eps)
|
|
658
|
+
max_step = step_factor * prev_step
|
|
659
|
+
big = numpy.abs(dz) > max_step
|
|
660
|
+
dz[big] *= max_step[big] / numpy.abs(dz[big])
|
|
661
|
+
|
|
662
|
+
z2 = z1 - dz
|
|
663
|
+
f2 = f(z2) - a
|
|
664
|
+
|
|
665
|
+
# Line search by backtracking
|
|
666
|
+
worse = (numpy.abs(f2) >= numpy.abs(f1)) & active
|
|
667
|
+
if worse.any():
|
|
668
|
+
shrink = numpy.ones_like(dz)
|
|
669
|
+
for _ in range(max_bt):
|
|
670
|
+
shrink[worse] *= alpha
|
|
671
|
+
z_try = z1[worse] - shrink[worse] * dz[worse]
|
|
672
|
+
f_try = f(z_try) - a[worse]
|
|
673
|
+
|
|
674
|
+
improved = numpy.abs(f_try) < numpy.abs(f1[worse])
|
|
675
|
+
if not improved.any():
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
idx = numpy.flatnonzero(worse)[improved]
|
|
679
|
+
z2[idx], f2[idx] = z_try[improved], f_try[improved]
|
|
680
|
+
worse[idx] = False
|
|
681
|
+
if not worse.any():
|
|
682
|
+
break
|
|
683
|
+
|
|
684
|
+
# Book-keeping
|
|
685
|
+
newly_conv = (numpy.abs(f2) < tol) & active
|
|
686
|
+
converged[newly_conv] = True
|
|
687
|
+
iterations[newly_conv] = k + 1
|
|
688
|
+
roots[newly_conv] = z2[newly_conv]
|
|
689
|
+
residuals[newly_conv] = numpy.abs(f2[newly_conv])
|
|
690
|
+
|
|
691
|
+
still = active & ~newly_conv
|
|
692
|
+
z0[still], z1[still] = z1[still], z2[still]
|
|
693
|
+
f0[still], f1[still] = f1[still], f2[still]
|
|
694
|
+
|
|
695
|
+
if verbose and k % 10 == 0:
|
|
696
|
+
print(f"Iter {k}: {converged.sum()} / {n_points} converged")
|
|
697
|
+
|
|
698
|
+
# Non-converged points
|
|
699
|
+
remaining = ~converged
|
|
700
|
+
roots[remaining] = z1[remaining]
|
|
701
|
+
residuals[remaining] = numpy.abs(f1[remaining])
|
|
702
|
+
iterations[remaining] = max_iter
|
|
703
|
+
|
|
704
|
+
# Optional clean-up pass
|
|
705
|
+
if post_smooth and n_points > 2:
|
|
706
|
+
# absolute jump to *nearest* neighbour (left or right)
|
|
707
|
+
diff_left = numpy.empty_like(roots)
|
|
708
|
+
diff_right = numpy.empty_like(roots)
|
|
709
|
+
diff_left[1:] = numpy.abs(roots[1:] - roots[:-1])
|
|
710
|
+
diff_right[:-1] = numpy.abs(roots[:-1] - roots[1:])
|
|
711
|
+
jump = numpy.minimum(diff_left, diff_right)
|
|
712
|
+
|
|
713
|
+
# ignore non-converged points
|
|
714
|
+
median_jump = numpy.median(jump[~remaining])
|
|
715
|
+
bad = (jump > jump_tol * median_jump) & ~remaining
|
|
716
|
+
|
|
717
|
+
if bad.any():
|
|
718
|
+
z_first_all = numpy.where(bad & (diff_left <= diff_right),
|
|
719
|
+
roots - diff_left,
|
|
720
|
+
roots + diff_right)
|
|
721
|
+
|
|
722
|
+
# keep only the offending indices
|
|
723
|
+
z_first = z_first_all[bad]
|
|
724
|
+
z_second = z_first + (roots[bad] - z_first) * 1e-2
|
|
725
|
+
|
|
726
|
+
# re-solve just the outliers in one vector call
|
|
727
|
+
new_root, new_res, new_iter = _secant_complex(
|
|
728
|
+
f, z_first, z_second, a[bad], tol=tol, max_iter=max_iter,
|
|
729
|
+
alpha=alpha, max_bt=max_bt, eps=eps, step_factor=step_factor,
|
|
730
|
+
dtype=dtype, post_smooth=False, # avoid recursion
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
roots[bad] = new_root
|
|
734
|
+
residuals[bad] = new_res
|
|
735
|
+
iterations[bad] = iterations[bad] + new_iter
|
|
736
|
+
|
|
737
|
+
if verbose:
|
|
738
|
+
print(f"Clean-up: re-solved {bad.sum()} outliers")
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
roots.reshape(orig_shape),
|
|
742
|
+
residuals.reshape(orig_shape),
|
|
743
|
+
iterations.reshape(orig_shape),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ================
|
|
748
|
+
# plot diagnostics
|
|
749
|
+
# ================
|
|
750
|
+
|
|
751
|
+
def _plot_diagnostics(freeform, x, roots, residuals, iterations, tolerance,
|
|
752
|
+
max_iter):
|
|
753
|
+
"""
|
|
754
|
+
Plot the results of root-findings, including the residuals, number of
|
|
755
|
+
iterations, and the imaginary part of the roots.
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
with texplot.theme(use_latex=False):
|
|
759
|
+
fig, ax = plt.subplots(ncols=3, figsize=(9.5, 3))
|
|
760
|
+
|
|
761
|
+
for i in range(ax.size):
|
|
762
|
+
ax[i].axvline(x=freeform.lam_m, color='silver', linestyle=':')
|
|
763
|
+
ax[i].axvline(x=freeform.lam_p, color='silver', linestyle=':')
|
|
764
|
+
|
|
765
|
+
ax[0].axhline(y=tolerance, color='silver', linestyle='--',
|
|
766
|
+
label='tolerance')
|
|
767
|
+
ax[0].plot(x, numpy.abs(residuals), color='black')
|
|
768
|
+
|
|
769
|
+
ax[1].axhline(y=max_iter, color='silver', linestyle='--',
|
|
770
|
+
label='max iter')
|
|
771
|
+
ax[1].plot(x, iterations, color='black')
|
|
772
|
+
|
|
773
|
+
ax[2].axhline(y=0, color='silver', linestyle='-', alpha=0.5)
|
|
774
|
+
ax[2].plot(x, roots.imag, color='black')
|
|
775
|
+
|
|
776
|
+
ax[0].set_yscale('log')
|
|
777
|
+
ax[0].set_xlim([x[0], x[-1]])
|
|
778
|
+
ax[0].set_title('Residuals')
|
|
779
|
+
ax[0].set_xlabel(r'$x$')
|
|
780
|
+
ax[0].set_ylabel(r'$| f(z_0) - z |$')
|
|
781
|
+
ax[0].legend(fontsize='xx-small')
|
|
782
|
+
|
|
783
|
+
ax[1].set_xlim([x[0], x[-1]])
|
|
784
|
+
ax[1].set_title('Iterations')
|
|
785
|
+
ax[1].set_xlabel(r'$x$')
|
|
786
|
+
ax[1].set_ylabel(r'$n$')
|
|
787
|
+
ax[1].legend(fontsize='xx-small')
|
|
788
|
+
|
|
789
|
+
ax[2].set_xlim([x[0], x[-1]])
|
|
790
|
+
ax[2].set_title('Roots Imaginary Part')
|
|
791
|
+
ax[2].set_xlabel(r'$x$')
|
|
792
|
+
ax[2].set_ylabel(r'Im$(z_0)$')
|
|
793
|
+
ax[2].set_yscale('symlog', linthresh=1e-5)
|
|
794
|
+
|
|
795
|
+
plt.tight_layout()
|
|
796
|
+
plt.show()
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# ==========
|
|
800
|
+
# decompress
|
|
801
|
+
# ==========
|
|
802
|
+
|
|
803
|
+
def decompress(freeform, alpha, x, roots_init=None, method='newton',
|
|
804
|
+
delta=1e-4, max_iter=500, step_size=0.1, tolerance=1e-4,
|
|
805
|
+
plot_diagnostics=False):
|
|
806
|
+
"""
|
|
807
|
+
Free decompression of spectral density.
|
|
808
|
+
|
|
809
|
+
Parameters
|
|
810
|
+
----------
|
|
811
|
+
|
|
812
|
+
freeform : FreeForm
|
|
813
|
+
The initial freeform object of matrix to be decompressed
|
|
814
|
+
|
|
815
|
+
alpha : float
|
|
816
|
+
Decompression ratio :math:`\\alpha = n / n_s = e^{t}`.
|
|
817
|
+
|
|
818
|
+
x : numpy.array
|
|
819
|
+
Positions where density to be evaluated at.
|
|
820
|
+
|
|
821
|
+
roots_init : numpy.array, default=None
|
|
822
|
+
Initial guess for roots. If `None`, all root points are allocated at
|
|
823
|
+
a point below the center of support. If given, this is usually the
|
|
824
|
+
root that is found in the previous iteration of the called function.
|
|
825
|
+
|
|
826
|
+
method : {``'newton'``, ``'secant'``}, default=``'newton'``
|
|
827
|
+
Root-finding method.
|
|
828
|
+
|
|
829
|
+
delta: float, default=1e-4
|
|
830
|
+
Size of the perturbation into the upper half plane for Plemelj's
|
|
831
|
+
formula.
|
|
832
|
+
|
|
833
|
+
max_iter: int, default=500
|
|
834
|
+
Maximum number of iterations of the chosen method.
|
|
835
|
+
|
|
836
|
+
step_size: float, default=0.1
|
|
837
|
+
Step size for Newton iterations.
|
|
838
|
+
|
|
839
|
+
tolerance: float, default=1e-4
|
|
840
|
+
Tolerance for the solution obtained by the secant method solver.
|
|
841
|
+
|
|
842
|
+
plot_diagnostics : bool, default=False
|
|
843
|
+
Plots diagnostics including convergence and number of iterations of
|
|
844
|
+
root finding method.
|
|
845
|
+
|
|
846
|
+
Returns
|
|
847
|
+
-------
|
|
848
|
+
|
|
849
|
+
rho : numpy.array
|
|
850
|
+
Spectral density
|
|
851
|
+
|
|
852
|
+
See Also
|
|
853
|
+
--------
|
|
854
|
+
|
|
855
|
+
density
|
|
856
|
+
stieltjes
|
|
857
|
+
|
|
858
|
+
Examples
|
|
859
|
+
--------
|
|
860
|
+
|
|
861
|
+
.. code-block:: python
|
|
862
|
+
|
|
863
|
+
>>> from freealg import FreeForm
|
|
864
|
+
"""
|
|
865
|
+
|
|
866
|
+
# Locations where Stieltjes is sought to be found at.
|
|
867
|
+
target = x + delta * 1j
|
|
868
|
+
|
|
869
|
+
if numpy.isclose(alpha, 1.0):
|
|
870
|
+
return freeform.density(x), roots_init
|
|
871
|
+
|
|
872
|
+
# Function that returns the second branch of Stieltjes
|
|
873
|
+
m = freeform._eval_stieltjes
|
|
874
|
+
|
|
875
|
+
# ------
|
|
876
|
+
# char z
|
|
877
|
+
# ------
|
|
878
|
+
|
|
879
|
+
def _char_z(z_0):
|
|
880
|
+
"""
|
|
881
|
+
Characteristic curve map. Returns z from initial z_0.
|
|
882
|
+
"""
|
|
883
|
+
|
|
884
|
+
return z_0 + (1.0 / m(z_0)) * (1.0 - alpha)
|
|
885
|
+
|
|
886
|
+
# -----
|
|
887
|
+
|
|
888
|
+
# Initialize roots below the real axis
|
|
889
|
+
if roots_init is None:
|
|
890
|
+
roots_init = numpy.full(x.shape, numpy.mean(freeform.support) - 0.1j,
|
|
891
|
+
dtype=freeform.dtype)
|
|
892
|
+
|
|
893
|
+
# Finding roots
|
|
894
|
+
if method == 'newton':
|
|
895
|
+
support = (freeform.lam_m, freeform.lam_p)
|
|
896
|
+
|
|
897
|
+
# Using initial points below the real line.
|
|
898
|
+
roots, residuals, iterations = \
|
|
899
|
+
_newton_method(_char_z, roots_init, target, support,
|
|
900
|
+
enforce_wall=False, tol=tolerance,
|
|
901
|
+
step_size=step_size, max_iter=max_iter,
|
|
902
|
+
adaptive_stencil=True, halley=False, vertical=False)
|
|
903
|
+
|
|
904
|
+
# Using target points themselves as initial points. Since these points
|
|
905
|
+
# are above the real line, enforce branchcut as wall
|
|
906
|
+
# roots_init_1 = target
|
|
907
|
+
# roots_1, residuals_1, iterations_1 = \
|
|
908
|
+
# _newton_method(_char_z, roots_init_1, target, support,
|
|
909
|
+
# enforce_wall=True, tol=tolerance,
|
|
910
|
+
# step_size=step_size, max_iter=max_iter,
|
|
911
|
+
# adaptive_stencil=True, halley=False,
|
|
912
|
+
# vertical=True)
|
|
913
|
+
#
|
|
914
|
+
# # If any result from residuals1 is better than residuals, use them
|
|
915
|
+
# good = numpy.abs(residuals_1) < numpy.abs(residuals)
|
|
916
|
+
# roots[good] = roots_1[good]
|
|
917
|
+
# residuals[good] = residuals_1[good]
|
|
918
|
+
# iterations[good] = iterations_1[good]
|
|
919
|
+
|
|
920
|
+
elif method == 'secant':
|
|
921
|
+
z0 = numpy.full(x.shape, numpy.mean(freeform.support) + 0.1j,
|
|
922
|
+
dtype=freeform.dtype)
|
|
923
|
+
z1 = z0 - 0.2j
|
|
924
|
+
|
|
925
|
+
roots, _, _ = _secant_complex(_char_z, z0, z1, a=target, tol=tolerance,
|
|
926
|
+
max_iter=max_iter, dtype=freeform.dtype)
|
|
927
|
+
else:
|
|
928
|
+
raise NotImplementedError('"method" is invalid.')
|
|
929
|
+
|
|
930
|
+
# Plemelj's formula
|
|
931
|
+
z = roots
|
|
932
|
+
char_s = numpy.squeeze(m(z)) / alpha
|
|
933
|
+
rho = numpy.maximum(0, char_s.imag / numpy.pi)
|
|
934
|
+
|
|
935
|
+
# Check any nans are in the roots
|
|
936
|
+
num_nan = numpy.sum(numpy.isnan(roots))
|
|
937
|
+
if num_nan > 0:
|
|
938
|
+
raise RuntimeWarning(f'"nan" roots detected: num: {num_nan}.')
|
|
939
|
+
|
|
940
|
+
# dx = x[1] - x[0]
|
|
941
|
+
# left_idx, right_idx = support_from_density(dx, rho)
|
|
942
|
+
# x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
|
|
943
|
+
rho = rho / numpy.trapezoid(rho, x)
|
|
944
|
+
|
|
945
|
+
if plot_diagnostics:
|
|
946
|
+
_plot_diagnostics(freeform, x, roots, residuals, iterations, tolerance,
|
|
947
|
+
max_iter)
|
|
948
|
+
|
|
949
|
+
return rho, roots
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
# =======================
|
|
953
|
+
# reverse characteristics
|
|
954
|
+
# =======================
|
|
955
|
+
|
|
956
|
+
def reverse_characteristics(freeform, z_inits, T, iterations=500,
|
|
957
|
+
step_size=0.1, tolerance=1e-8,
|
|
958
|
+
dtype=numpy.complex128):
|
|
959
|
+
"""
|
|
960
|
+
"""
|
|
961
|
+
|
|
962
|
+
t_span = (0, T)
|
|
963
|
+
t_eval = numpy.linspace(t_span[0], t_span[1], 50)
|
|
964
|
+
|
|
965
|
+
m = freeform._eval_stieltjes
|
|
966
|
+
|
|
967
|
+
def _char_z(z, t):
|
|
968
|
+
return z + (1 / m(z)) * (1 - numpy.exp(t))
|
|
969
|
+
|
|
970
|
+
target_z, target_t = numpy.meshgrid(z_inits, t_eval)
|
|
971
|
+
|
|
972
|
+
z = numpy.full(target_z.shape, numpy.mean(freeform.support) - 0.1j,
|
|
973
|
+
dtype=dtype)
|
|
974
|
+
|
|
975
|
+
# Broken Newton steps can produce a lot of warnings. Removing them for now.
|
|
976
|
+
with numpy.errstate(all='ignore'):
|
|
977
|
+
for _ in range(iterations):
|
|
978
|
+
objective = _char_z(z, target_t) - target_z
|
|
979
|
+
mask = numpy.abs(objective) >= tolerance
|
|
980
|
+
if not numpy.any(mask):
|
|
981
|
+
break
|
|
982
|
+
z_m = z[mask]
|
|
983
|
+
t_m = target_t[mask]
|
|
984
|
+
|
|
985
|
+
# Perform finite difference approximation
|
|
986
|
+
dfdz = _char_z(z_m+tolerance, t_m) - _char_z(z_m-tolerance, t_m)
|
|
987
|
+
dfdz /= 2*tolerance
|
|
988
|
+
dfdz[dfdz == 0] = 1.0
|
|
989
|
+
|
|
990
|
+
# Perform Newton step
|
|
991
|
+
z[mask] = z_m - step_size * objective[mask] / dfdz
|
|
992
|
+
|
|
993
|
+
return z
|