freealg 0.4.1__py3-none-any.whl → 0.5.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.
- freealg/__version__.py +1 -1
- freealg/_chebyshev.py +36 -14
- freealg/_decompress.py +706 -66
- freealg/_linalg.py +24 -10
- freealg/_pade.py +3 -59
- freealg/_plot_util.py +30 -18
- freealg/_sample.py +13 -6
- freealg/_series.py +123 -0
- freealg/_support.py +2 -0
- freealg/_util.py +1 -1
- freealg/distributions/_kesten_mckay.py +6 -6
- freealg/distributions/_marchenko_pastur.py +9 -11
- freealg/distributions/_meixner.py +6 -6
- freealg/distributions/_wachter.py +9 -11
- freealg/distributions/_wigner.py +8 -9
- freealg/freeform.py +199 -82
- {freealg-0.4.1.dist-info → freealg-0.5.0.dist-info}/METADATA +1 -1
- freealg-0.5.0.dist-info/RECORD +26 -0
- freealg-0.4.1.dist-info/RECORD +0 -25
- {freealg-0.4.1.dist-info → freealg-0.5.0.dist-info}/WHEEL +0 -0
- {freealg-0.4.1.dist-info → freealg-0.5.0.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.4.1.dist-info → freealg-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.4.1.dist-info → freealg-0.5.0.dist-info}/top_level.txt +0 -0
freealg/_decompress.py
CHANGED
|
@@ -11,24 +11,566 @@
|
|
|
11
11
|
# =======
|
|
12
12
|
|
|
13
13
|
import numpy
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import texplot
|
|
14
16
|
|
|
15
|
-
# Fallback to previous API
|
|
17
|
+
# Fallback to previous numpy API
|
|
16
18
|
if not hasattr(numpy, 'trapezoid'):
|
|
17
19
|
numpy.trapezoid = numpy.trapz
|
|
18
20
|
|
|
19
21
|
__all__ = ['decompress', 'reverse_characteristics']
|
|
20
22
|
|
|
21
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
|
+
|
|
22
564
|
# =============
|
|
23
565
|
# secant method
|
|
24
566
|
# =============
|
|
25
567
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
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, verbose=False):
|
|
29
571
|
"""
|
|
30
|
-
Solves :math:``f(z) = a`` for many starting points simultaneously
|
|
31
|
-
|
|
572
|
+
Solves :math:``f(z) = a`` for many starting points simultaneously using the
|
|
573
|
+
secant method in the complex plane.
|
|
32
574
|
|
|
33
575
|
Parameters
|
|
34
576
|
----------
|
|
@@ -164,7 +706,7 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
|
|
|
164
706
|
diff_right[:-1] = numpy.abs(roots[:-1] - roots[1:])
|
|
165
707
|
jump = numpy.minimum(diff_left, diff_right)
|
|
166
708
|
|
|
167
|
-
# ignore
|
|
709
|
+
# ignore non-converged points
|
|
168
710
|
median_jump = numpy.median(jump[~remaining])
|
|
169
711
|
bad = (jump > jump_tol * median_jump) & ~remaining
|
|
170
712
|
|
|
@@ -178,13 +720,12 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
|
|
|
178
720
|
z_second = z_first + (roots[bad] - z_first) * 1e-2
|
|
179
721
|
|
|
180
722
|
# re-solve just the outliers in one vector call
|
|
181
|
-
new_root, new_res, new_iter =
|
|
182
|
-
f, z_first, z_second, a[bad],
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
eps=eps, step_factor=step_factor,
|
|
186
|
-
post_smooth=False, # avoid recursion
|
|
723
|
+
new_root, new_res, new_iter = _secant_complex(
|
|
724
|
+
f, z_first, z_second, a[bad], tol=tol, max_iter=max_iter,
|
|
725
|
+
alpha=alpha, max_bt=max_bt, eps=eps, step_factor=step_factor,
|
|
726
|
+
post_smooth=False, # avoid recursion
|
|
187
727
|
)
|
|
728
|
+
|
|
188
729
|
roots[bad] = new_root
|
|
189
730
|
residuals[bad] = new_res
|
|
190
731
|
iterations[bad] = iterations[bad] + new_iter
|
|
@@ -199,12 +740,65 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
|
|
|
199
740
|
)
|
|
200
741
|
|
|
201
742
|
|
|
743
|
+
# ================
|
|
744
|
+
# plot diagnostics
|
|
745
|
+
# ================
|
|
746
|
+
|
|
747
|
+
def _plot_diagnostics(freeform, x, roots, residuals, iterations, tolerance,
|
|
748
|
+
max_iter):
|
|
749
|
+
"""
|
|
750
|
+
Plot the results of root-findings, including the residuals, number of
|
|
751
|
+
iterations, and the imaginary part of the roots.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
with texplot.theme(use_latex=False):
|
|
755
|
+
fig, ax = plt.subplots(ncols=3, figsize=(9.5, 3))
|
|
756
|
+
|
|
757
|
+
for i in range(ax.size):
|
|
758
|
+
ax[i].axvline(x=freeform.lam_m, color='silver', linestyle=':')
|
|
759
|
+
ax[i].axvline(x=freeform.lam_p, color='silver', linestyle=':')
|
|
760
|
+
|
|
761
|
+
ax[0].axhline(y=tolerance, color='silver', linestyle='--',
|
|
762
|
+
label='tolerance')
|
|
763
|
+
ax[0].plot(x, numpy.abs(residuals), color='black')
|
|
764
|
+
|
|
765
|
+
ax[1].axhline(y=max_iter, color='silver', linestyle='--',
|
|
766
|
+
label='max iter')
|
|
767
|
+
ax[1].plot(x, iterations, color='black')
|
|
768
|
+
|
|
769
|
+
ax[2].axhline(y=0, color='silver', linestyle='-', alpha=0.5)
|
|
770
|
+
ax[2].plot(x, roots.imag, color='black')
|
|
771
|
+
|
|
772
|
+
ax[0].set_yscale('log')
|
|
773
|
+
ax[0].set_xlim([x[0], x[-1]])
|
|
774
|
+
ax[0].set_title('Residuals')
|
|
775
|
+
ax[0].set_xlabel(r'$x$')
|
|
776
|
+
ax[0].set_ylabel(r'$| f(z_0) - z |$')
|
|
777
|
+
ax[0].legend(fontsize='xx-small')
|
|
778
|
+
|
|
779
|
+
ax[1].set_xlim([x[0], x[-1]])
|
|
780
|
+
ax[1].set_title('Iterations')
|
|
781
|
+
ax[1].set_xlabel(r'$x$')
|
|
782
|
+
ax[1].set_ylabel(r'$n$')
|
|
783
|
+
ax[1].legend(fontsize='xx-small')
|
|
784
|
+
|
|
785
|
+
ax[2].set_xlim([x[0], x[-1]])
|
|
786
|
+
ax[2].set_title('Roots Imaginary Part')
|
|
787
|
+
ax[2].set_xlabel(r'$x$')
|
|
788
|
+
ax[2].set_ylabel(r'Im$(z_0)$')
|
|
789
|
+
ax[2].set_yscale('symlog', linthresh=1e-5)
|
|
790
|
+
|
|
791
|
+
plt.tight_layout()
|
|
792
|
+
plt.show()
|
|
793
|
+
|
|
794
|
+
|
|
202
795
|
# ==========
|
|
203
796
|
# decompress
|
|
204
797
|
# ==========
|
|
205
798
|
|
|
206
|
-
def decompress(freeform,
|
|
207
|
-
tolerance=1e-
|
|
799
|
+
def decompress(freeform, alpha, x, roots_init=None, method='newton',
|
|
800
|
+
delta=1e-4, max_iter=500, step_size=0.1, tolerance=1e-4,
|
|
801
|
+
plot_diagnostics=False):
|
|
208
802
|
"""
|
|
209
803
|
Free decompression of spectral density.
|
|
210
804
|
|
|
@@ -214,23 +808,37 @@ def decompress(freeform, size, x=None, delta=1e-4, max_iter=500,
|
|
|
214
808
|
freeform : FreeForm
|
|
215
809
|
The initial freeform object of matrix to be decompressed
|
|
216
810
|
|
|
217
|
-
|
|
218
|
-
|
|
811
|
+
alpha : float
|
|
812
|
+
Decompression ratio :math:`\\alpha = n / n_s = e^{t}`.
|
|
219
813
|
|
|
220
|
-
x : numpy.array
|
|
221
|
-
Positions where density to be evaluated at.
|
|
222
|
-
|
|
814
|
+
x : numpy.array
|
|
815
|
+
Positions where density to be evaluated at.
|
|
816
|
+
|
|
817
|
+
roots_init : numpy.array, default=None
|
|
818
|
+
Initial guess for roots. If `None`, all root points are allocated at
|
|
819
|
+
a point below the center of support. If given, this is usually the
|
|
820
|
+
root that is found in the previous iteration of the called function.
|
|
821
|
+
|
|
822
|
+
method : {``'newton'``, ``'secant'``}, default=``'newton'``
|
|
823
|
+
Root-finding method.
|
|
223
824
|
|
|
224
825
|
delta: float, default=1e-4
|
|
225
826
|
Size of the perturbation into the upper half plane for Plemelj's
|
|
226
827
|
formula.
|
|
227
828
|
|
|
228
829
|
max_iter: int, default=500
|
|
229
|
-
Maximum number of
|
|
830
|
+
Maximum number of iterations of the chosen method.
|
|
831
|
+
|
|
832
|
+
step_size: float, default=0.1
|
|
833
|
+
Step size for Newton iterations.
|
|
230
834
|
|
|
231
|
-
tolerance: float, default=1e-
|
|
835
|
+
tolerance: float, default=1e-4
|
|
232
836
|
Tolerance for the solution obtained by the secant method solver.
|
|
233
837
|
|
|
838
|
+
plot_diagnostics : bool, default=False
|
|
839
|
+
Plots diagnostics including convergence and number of iterations of
|
|
840
|
+
root finding method.
|
|
841
|
+
|
|
234
842
|
Returns
|
|
235
843
|
-------
|
|
236
844
|
|
|
@@ -261,58 +869,90 @@ def decompress(freeform, size, x=None, delta=1e-4, max_iter=500,
|
|
|
261
869
|
>>> from freealg import FreeForm
|
|
262
870
|
"""
|
|
263
871
|
|
|
264
|
-
|
|
265
|
-
m = freeform._eval_stieltjes
|
|
266
|
-
# Lower and upper bound on new support
|
|
267
|
-
hilb_lb = (1 / m(freeform.lam_m + delta * 1j)).real
|
|
268
|
-
hilb_ub = (1 / m(freeform.lam_p + delta * 1j)).real
|
|
269
|
-
lb = freeform.lam_m - (alpha - 1) * hilb_lb
|
|
270
|
-
ub = freeform.lam_p - (alpha - 1) * hilb_ub
|
|
271
|
-
|
|
272
|
-
# Create x if not given
|
|
273
|
-
on_grid = (x is None)
|
|
274
|
-
if on_grid:
|
|
275
|
-
radius = 0.5 * (ub - lb)
|
|
276
|
-
center = 0.5 * (ub + lb)
|
|
277
|
-
scale = 1.25
|
|
278
|
-
x_min = numpy.floor(center - radius * scale)
|
|
279
|
-
x_max = numpy.ceil(center + radius * scale)
|
|
280
|
-
x = numpy.linspace(x_min, x_max, 500)
|
|
281
|
-
else:
|
|
282
|
-
x = numpy.asarray(x)
|
|
283
|
-
|
|
872
|
+
# Locations where Stieltjes is sought to be found at.
|
|
284
873
|
target = x + delta * 1j
|
|
285
|
-
if numpy.isclose(alpha, 1.0):
|
|
286
|
-
return freeform.density(x), x, freeform.support
|
|
287
874
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return z + (1 / m(z)) * (1 - alpha)
|
|
875
|
+
if numpy.isclose(alpha, 1.0):
|
|
876
|
+
return freeform.density(x), roots_init
|
|
291
877
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
z1 = z0 - 0.2j
|
|
878
|
+
# Function that returns the second branch of Stieltjes
|
|
879
|
+
m = freeform._eval_stieltjes
|
|
295
880
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
881
|
+
# ------
|
|
882
|
+
# char z
|
|
883
|
+
# ------
|
|
884
|
+
|
|
885
|
+
def _char_z(z_0):
|
|
886
|
+
"""
|
|
887
|
+
Characteristic curve map. Returns z from initial z_0.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
return z_0 + (1.0 / m(z_0)) * (1.0 - alpha)
|
|
891
|
+
|
|
892
|
+
# -----
|
|
893
|
+
|
|
894
|
+
# Initialize roots below the real axis
|
|
895
|
+
if roots_init is None:
|
|
896
|
+
roots_init = numpy.full(x.shape, numpy.mean(freeform.support) - 0.1j,
|
|
897
|
+
dtype=numpy.complex128)
|
|
898
|
+
|
|
899
|
+
# Finding roots
|
|
900
|
+
if method == 'newton':
|
|
901
|
+
support = (freeform.lam_m, freeform.lam_p)
|
|
902
|
+
|
|
903
|
+
# Using initial points below the real line.
|
|
904
|
+
roots, residuals, iterations = \
|
|
905
|
+
_newton_method(_char_z, roots_init, target, support,
|
|
906
|
+
enforce_wall=False, tol=tolerance,
|
|
907
|
+
step_size=step_size, max_iter=max_iter,
|
|
908
|
+
adaptive_stencil=True, halley=False, vertical=False)
|
|
909
|
+
|
|
910
|
+
# Using target points themselves as initial points. Since these points
|
|
911
|
+
# are above the real line, enforce branchcut as wall
|
|
912
|
+
# roots_init_1 = target
|
|
913
|
+
# roots_1, residuals_1, iterations_1 = \
|
|
914
|
+
# _newton_method(_char_z, roots_init_1, target, support,
|
|
915
|
+
# enforce_wall=True, tol=tolerance,
|
|
916
|
+
# step_size=step_size, max_iter=max_iter,
|
|
917
|
+
# adaptive_stencil=True, halley=False,
|
|
918
|
+
# vertical=True)
|
|
919
|
+
#
|
|
920
|
+
# # If any result from residuals1 is better than residuals, use them
|
|
921
|
+
# good = numpy.abs(residuals_1) < numpy.abs(residuals)
|
|
922
|
+
# roots[good] = roots_1[good]
|
|
923
|
+
# residuals[good] = residuals_1[good]
|
|
924
|
+
# iterations[good] = iterations_1[good]
|
|
925
|
+
|
|
926
|
+
elif method == 'secant':
|
|
927
|
+
z0 = numpy.full(x.shape, numpy.mean(freeform.support) + 0.1j,
|
|
928
|
+
dtype=numpy.complex128)
|
|
929
|
+
z1 = z0 - 0.2j
|
|
930
|
+
|
|
931
|
+
roots, _, _ = _secant_complex(_char_z, z0, z1, a=target, tol=tolerance,
|
|
932
|
+
max_iter=max_iter)
|
|
933
|
+
else:
|
|
934
|
+
raise NotImplementedError('"method" is invalid.')
|
|
302
935
|
|
|
303
936
|
# Plemelj's formula
|
|
304
937
|
z = roots
|
|
305
|
-
char_s = m(z) / alpha
|
|
938
|
+
char_s = numpy.squeeze(m(z)) / alpha
|
|
306
939
|
rho = numpy.maximum(0, char_s.imag / numpy.pi)
|
|
307
|
-
rho[numpy.isnan(rho) | numpy.isinf(rho)] = 0
|
|
308
|
-
if on_grid:
|
|
309
|
-
x, rho = x.ravel(), rho.ravel()
|
|
310
|
-
# dx = x[1] - x[0]
|
|
311
|
-
# left_idx, right_idx = support_from_density(dx, rho)
|
|
312
|
-
# x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
|
|
313
|
-
rho = rho / numpy.trapezoid(rho, x)
|
|
314
940
|
|
|
315
|
-
|
|
941
|
+
# Check any nans are in the roots
|
|
942
|
+
num_nan = numpy.sum(numpy.isnan(roots))
|
|
943
|
+
if num_nan > 0:
|
|
944
|
+
raise RuntimeWarning(f'"nan" roots detected: num: {num_nan}.')
|
|
945
|
+
|
|
946
|
+
# dx = x[1] - x[0]
|
|
947
|
+
# left_idx, right_idx = support_from_density(dx, rho)
|
|
948
|
+
# x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
|
|
949
|
+
rho = rho / numpy.trapezoid(rho, x)
|
|
950
|
+
|
|
951
|
+
if plot_diagnostics:
|
|
952
|
+
_plot_diagnostics(freeform, x, roots, residuals, iterations, tolerance,
|
|
953
|
+
max_iter)
|
|
954
|
+
|
|
955
|
+
return rho, roots
|
|
316
956
|
|
|
317
957
|
|
|
318
958
|
# =======================
|
|
@@ -334,7 +974,7 @@ def reverse_characteristics(freeform, z_inits, T, iterations=500,
|
|
|
334
974
|
|
|
335
975
|
target_z, target_t = numpy.meshgrid(z_inits, t_eval)
|
|
336
976
|
|
|
337
|
-
z = numpy.full(target_z.shape, numpy.mean(freeform.support) - .1j,
|
|
977
|
+
z = numpy.full(target_z.shape, numpy.mean(freeform.support) - 0.1j,
|
|
338
978
|
dtype=numpy.complex128)
|
|
339
979
|
|
|
340
980
|
# Broken Newton steps can produce a lot of warnings. Removing them for now.
|