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.
Files changed (59) hide show
  1. freealg/__init__.py +8 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +12 -0
  4. freealg/_algebraic_form/_branch_points.py +288 -0
  5. freealg/_algebraic_form/_constraints.py +139 -0
  6. freealg/_algebraic_form/_continuation_algebraic.py +706 -0
  7. freealg/_algebraic_form/_decompress.py +641 -0
  8. freealg/_algebraic_form/_decompress2.py +204 -0
  9. freealg/_algebraic_form/_edge.py +330 -0
  10. freealg/_algebraic_form/_homotopy.py +323 -0
  11. freealg/_algebraic_form/_moments.py +448 -0
  12. freealg/_algebraic_form/_sheets_util.py +145 -0
  13. freealg/_algebraic_form/_support.py +309 -0
  14. freealg/_algebraic_form/algebraic_form.py +1232 -0
  15. freealg/_free_form/__init__.py +16 -0
  16. freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
  17. freealg/_free_form/_decompress.py +993 -0
  18. freealg/_free_form/_density_util.py +243 -0
  19. freealg/_free_form/_jacobi.py +359 -0
  20. freealg/_free_form/_linalg.py +508 -0
  21. freealg/{_pade.py → _free_form/_pade.py} +42 -208
  22. freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
  23. freealg/{_sample.py → _free_form/_sample.py} +58 -22
  24. freealg/_free_form/_series.py +454 -0
  25. freealg/_free_form/_support.py +214 -0
  26. freealg/_free_form/free_form.py +1362 -0
  27. freealg/_geometric_form/__init__.py +13 -0
  28. freealg/_geometric_form/_continuation_genus0.py +175 -0
  29. freealg/_geometric_form/_continuation_genus1.py +275 -0
  30. freealg/_geometric_form/_elliptic_functions.py +174 -0
  31. freealg/_geometric_form/_sphere_maps.py +63 -0
  32. freealg/_geometric_form/_torus_maps.py +118 -0
  33. freealg/_geometric_form/geometric_form.py +1094 -0
  34. freealg/_util.py +56 -110
  35. freealg/distributions/__init__.py +7 -1
  36. freealg/distributions/_chiral_block.py +494 -0
  37. freealg/distributions/_deformed_marchenko_pastur.py +726 -0
  38. freealg/distributions/_deformed_wigner.py +386 -0
  39. freealg/distributions/_kesten_mckay.py +29 -15
  40. freealg/distributions/_marchenko_pastur.py +224 -95
  41. freealg/distributions/_meixner.py +47 -37
  42. freealg/distributions/_wachter.py +29 -17
  43. freealg/distributions/_wigner.py +27 -14
  44. freealg/visualization/__init__.py +12 -0
  45. freealg/visualization/_glue_util.py +32 -0
  46. freealg/visualization/_rgb_hsv.py +125 -0
  47. freealg-0.7.12.dist-info/METADATA +172 -0
  48. freealg-0.7.12.dist-info/RECORD +53 -0
  49. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
  50. freealg/_decompress.py +0 -180
  51. freealg/_jacobi.py +0 -218
  52. freealg/_support.py +0 -85
  53. freealg/freeform.py +0 -967
  54. freealg-0.1.11.dist-info/METADATA +0 -140
  55. freealg-0.1.11.dist-info/RECORD +0 -24
  56. /freealg/{_damp.py → _free_form/_damp.py} +0 -0
  57. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
  58. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
  59. {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