freealg 0.4.1__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
freealg/_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 secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
27
- alpha=0.5, max_bt=1, eps=1e-30, step_factor=5.0,
28
- post_smooth=True, jump_tol=10.0, verbose=False):
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
- using the secant method in the complex plane.
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 unconverged points
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 = secant_complex(
182
- f, z_first, z_second, a[bad],
183
- tol=tol, max_iter=max_iter,
184
- alpha=alpha, max_bt=max_bt,
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, size, x=None, delta=1e-4, max_iter=500,
207
- tolerance=1e-8):
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
- size : int
218
- Size of the decompressed matrix.
811
+ alpha : float
812
+ Decompression ratio :math:`\\alpha = n / n_s = e^{t}`.
219
813
 
220
- x : numpy.array, default=None
221
- Positions where density to be evaluated at. If `None`, an interval
222
- slightly larger than the support interval will be used.
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 secant method iterations.
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-12
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
- alpha = size / freeform.n
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
- # Characteristic curve map
289
- def _char_z(z):
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
- z0 = numpy.full(target.shape, numpy.mean(freeform.support) + 0.1j,
293
- dtype=numpy.complex128)
294
- z1 = z0 - 0.2j
878
+ # Function that returns the second branch of Stieltjes
879
+ m = freeform._eval_stieltjes
295
880
 
296
- roots, _, _ = secant_complex(
297
- _char_z, z0, z1,
298
- a=target,
299
- tol=tolerance,
300
- max_iter=max_iter
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
- return rho.reshape(*x.shape), x, (lb, ub)
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.