freealg 0.7.17__py3-none-any.whl → 0.7.18__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 (52) hide show
  1. freealg/__init__.py +8 -6
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/_branch_points.py +18 -18
  4. freealg/_algebraic_form/_continuation_algebraic.py +13 -13
  5. freealg/_algebraic_form/_cusp.py +15 -15
  6. freealg/_algebraic_form/_cusp_wrap.py +6 -6
  7. freealg/_algebraic_form/_decompress.py +16 -16
  8. freealg/_algebraic_form/_decompress4.py +31 -31
  9. freealg/_algebraic_form/_decompress5.py +23 -23
  10. freealg/_algebraic_form/_decompress6.py +13 -13
  11. freealg/_algebraic_form/_decompress7.py +15 -15
  12. freealg/_algebraic_form/_decompress8.py +17 -17
  13. freealg/_algebraic_form/_decompress9.py +18 -18
  14. freealg/_algebraic_form/_decompress_new.py +17 -17
  15. freealg/_algebraic_form/_decompress_new_2.py +57 -57
  16. freealg/_algebraic_form/_decompress_util.py +10 -10
  17. freealg/_algebraic_form/_decompressible.py +292 -0
  18. freealg/_algebraic_form/_edge.py +10 -10
  19. freealg/_algebraic_form/_homotopy4.py +9 -9
  20. freealg/_algebraic_form/_homotopy5.py +9 -9
  21. freealg/_algebraic_form/_support.py +19 -19
  22. freealg/_algebraic_form/algebraic_form.py +262 -468
  23. freealg/_base_form.py +401 -0
  24. freealg/_free_form/__init__.py +1 -4
  25. freealg/_free_form/_density_util.py +1 -1
  26. freealg/_free_form/_plot_util.py +3 -511
  27. freealg/_free_form/free_form.py +8 -367
  28. freealg/_util.py +59 -11
  29. freealg/distributions/__init__.py +2 -1
  30. freealg/distributions/_base_distribution.py +163 -0
  31. freealg/distributions/_chiral_block.py +137 -11
  32. freealg/distributions/_compound_poisson.py +141 -47
  33. freealg/distributions/_deformed_marchenko_pastur.py +138 -33
  34. freealg/distributions/_deformed_wigner.py +98 -9
  35. freealg/distributions/_fuss_catalan.py +269 -0
  36. freealg/distributions/_kesten_mckay.py +4 -130
  37. freealg/distributions/_marchenko_pastur.py +8 -196
  38. freealg/distributions/_meixner.py +4 -130
  39. freealg/distributions/_wachter.py +4 -130
  40. freealg/distributions/_wigner.py +10 -127
  41. freealg/visualization/__init__.py +2 -2
  42. freealg/visualization/{_rgb_hsv.py → _domain_coloring.py} +37 -29
  43. freealg/visualization/_plot_util.py +513 -0
  44. {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/METADATA +1 -1
  45. freealg-0.7.18.dist-info/RECORD +74 -0
  46. freealg-0.7.17.dist-info/RECORD +0 -69
  47. /freealg/{_free_form/_sample.py → _sample.py} +0 -0
  48. /freealg/{_free_form/_support.py → _support.py} +0 -0
  49. {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/WHEEL +0 -0
  50. {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/licenses/AUTHORS.txt +0 -0
  51. {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/licenses/LICENSE.txt +0 -0
  52. {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/top_level.txt +0 -0
@@ -12,13 +12,14 @@
12
12
  # =======
13
13
 
14
14
  import numpy
15
- from .._util import resolve_complex_dtype, compute_eig
15
+ from .._util import compute_eig
16
16
  # from .._util import compute_eig
17
17
  from ._continuation_algebraic import sample_z_joukowski, \
18
18
  filter_z_away_from_cuts, fit_polynomial_relation, \
19
19
  sanity_check_stieltjes_branch, eval_P
20
20
  from ._edge import evolve_edges, merge_edges
21
21
  from ._cusp_wrap import cusp_wrap
22
+ from ._decompressible import precheck_laurent
22
23
 
23
24
  # Decompress with Newton
24
25
  # from ._decompress import build_time_grid, decompress_newton
@@ -44,11 +45,13 @@ from ._decompress2 import decompress_coeffs, plot_candidates
44
45
  # from ._homotopy4 import StieltjesPoly
45
46
  from ._homotopy5 import StieltjesPoly
46
47
 
47
- from ._branch_points import compute_branch_points
48
- from ._support import compute_support
48
+ from ._branch_points import estimate_branch_points
49
+ from ._support import estimate_support
50
+ from .._support import supp as estimate_broad_supp
49
51
  from ._moments import Moments, AlgebraicStieltjesMoments
50
- from .._free_form._support import supp
51
- from .._free_form._plot_util import plot_density, plot_hilbert, plot_stieltjes
52
+ from ..visualization._plot_util import plot_density, plot_hilbert, \
53
+ plot_stieltjes
54
+ from .._base_form import BaseForm
52
55
 
53
56
  # Fallback to previous numpy API
54
57
  if not hasattr(numpy, 'trapezoid'):
@@ -61,7 +64,7 @@ __all__ = ['AlgebraicForm']
61
64
  # Algebraic Form
62
65
  # ==============
63
66
 
64
- class AlgebraicForm(object):
67
+ class AlgebraicForm(BaseForm):
65
68
  """
66
69
  Algebraic surrogate for ensemble models.
67
70
 
@@ -104,7 +107,7 @@ class AlgebraicForm(object):
104
107
  eig : numpy.array
105
108
  Eigenvalues of the matrix
106
109
 
107
- support: tuple
110
+ supp: tuple
108
111
  The predicted (or given) support :math:`(\\lambda_{\\min},
109
112
  \\lambda_{\\max})` of the eigenvalue density.
110
113
 
@@ -116,10 +119,16 @@ class AlgebraicForm(object):
116
119
  -------
117
120
 
118
121
  fit
119
- Fit the Jacobi polynomials to the empirical density.
122
+ Fit an algebraic structure to the input data
123
+
124
+ support
125
+ Estimate the spectral edges of the density
126
+
127
+ branch_points
128
+ Compute global branch points and zeros of leading coefficient
120
129
 
121
130
  density
122
- Compute the spectral density of the matrix.
131
+ Evaluate spectral density
123
132
 
124
133
  hilbert
125
134
  Compute Hilbert transform of the spectral density
@@ -130,6 +139,18 @@ class AlgebraicForm(object):
130
139
  decompress
131
140
  Free decompression of spectral density
132
141
 
142
+ candidate
143
+ Candidate densities of free decompression from all possible roots
144
+
145
+ is_decompressible
146
+ Check if the underlying distribution can be decompressed
147
+
148
+ edge
149
+ Evolves spectral edges
150
+
151
+ cusp
152
+ Find cusp (merge) point of evolving spectral edges
153
+
133
154
  eigvalsh
134
155
  Estimate the eigenvalues
135
156
 
@@ -150,7 +171,7 @@ class AlgebraicForm(object):
150
171
 
151
172
  .. code-block:: python
152
173
 
153
- >>> from freealg import FreeForm
174
+ >>> from freealg import AlgebraicForm
154
175
  """
155
176
 
156
177
  # ====
@@ -163,21 +184,17 @@ class AlgebraicForm(object):
163
184
  Initialization.
164
185
  """
165
186
 
166
- self.A = None
167
- self.eig = None
187
+ super().__init__(delta, dtype)
188
+
168
189
  self._stieltjes = None
169
190
  self._moments = None
170
- self.support = support
171
- self.est_support = None # Estimated from polynmial after fitting
172
- self.delta = delta # Offset above real axis to apply Plemelj formula
173
-
174
- # Data type for complex arrays
175
- self.dtype = resolve_complex_dtype(dtype)
191
+ self.supp = support
192
+ self.est_supp = None # Estimated from polynomial after fitting
176
193
 
177
194
  if hasattr(A, 'stieltjes') and callable(getattr(A, 'stieltjes', None)):
178
195
  # This is one of the distribution objects, like MarchenkoPastur
179
196
  self._stieltjes = A.stieltjes
180
- self.support = A.support()
197
+ self.supp = A.support()
181
198
  self.n = 1
182
199
 
183
200
  elif callable(A):
@@ -206,22 +223,21 @@ class AlgebraicForm(object):
206
223
  self._moments = Moments(self.eig) # NOTE (never used)
207
224
 
208
225
  # broad support
209
- if self.support is None:
226
+ if self.supp is None:
210
227
  if self.eig is None:
211
228
  raise RuntimeError("Support must be provided without data")
212
229
 
213
230
  # Detect support
214
- self.lam_m, self.lam_p = supp(self.eig, **kwargs)
215
- self.broad_support = (float(self.lam_m), float(self.lam_p))
231
+ self.lam_m, self.lam_p = estimate_broad_supp(self.eig, **kwargs)
232
+ self.broad_supp = (float(self.lam_m), float(self.lam_p))
216
233
  else:
217
- self.lam_m = float(min([s[0] for s in self.support]))
218
- self.lam_p = float(max([s[1] for s in self.support]))
219
- self.broad_support = (self.lam_m, self.lam_p)
234
+ self.lam_m = float(min([s[0] for s in self.supp]))
235
+ self.lam_p = float(max([s[1] for s in self.supp]))
236
+ self.broad_supp = (self.lam_m, self.lam_p)
220
237
 
221
238
  # Initialize
222
- self.a_coeffs = None # Polynomial coefficients
239
+ self.coeffs = None # Polynomial coefficients
223
240
  self.status = None # Fitting status
224
- self.cache = {} # Cache inner-computations
225
241
 
226
242
  # ===
227
243
  # fit
@@ -239,7 +255,7 @@ class AlgebraicForm(object):
239
255
  normalize=False,
240
256
  verbose=False):
241
257
  """
242
- Fit polynomial.
258
+ Fit an algebraic structure to the input data.
243
259
 
244
260
  Parameters
245
261
  ----------
@@ -275,14 +291,14 @@ class AlgebraicForm(object):
275
291
  z_fits = []
276
292
 
277
293
  # Sampling around support, or broad_support. This is only needed to
278
- # ensure sampled points are not hiting the support itself is not used
294
+ # ensure sampled points are not hitting the support itself is not used
279
295
  # in any computation. If support is not known, use broad support.
280
- if self.support is not None:
281
- possible_support = self.support
296
+ if self.supp is not None:
297
+ possible_supp = self.supp
282
298
  else:
283
- possible_support = [self.broad_support]
299
+ possible_supp = [self.broad_supp]
284
300
 
285
- for sup in possible_support:
301
+ for sup in possible_supp:
286
302
  a, b = sup
287
303
 
288
304
  for i in range(len(r)):
@@ -292,30 +308,28 @@ class AlgebraicForm(object):
292
308
  z_fit = numpy.concatenate(z_fits)
293
309
 
294
310
  # Remove points too close to any cut
295
- z_fit = filter_z_away_from_cuts(z_fit, possible_support, y_eps=y_eps,
311
+ z_fit = filter_z_away_from_cuts(z_fit, possible_supp, y_eps=y_eps,
296
312
  x_pad=x_pad)
297
313
 
298
314
  # Fitting (w_inf = None means adaptive weight selection)
299
315
  m1_fit = self._stieltjes(z_fit)
300
- a_coeffs, fit_metrics = fit_polynomial_relation(
316
+ self.coeffs, fit_metrics = fit_polynomial_relation(
301
317
  z_fit, m1_fit, s=deg_m, deg_z=deg_z, ridge_lambda=reg,
302
318
  triangular=triangular, normalize=normalize, mu=mu,
303
319
  mu_reg=mu_reg)
304
320
 
305
- self.a_coeffs = a_coeffs
306
-
307
321
  # Estimate support from the fitted polynomial
308
- self.est_support, _ = self.estimate_support(a_coeffs)
322
+ self.est_supp, _ = self.support(self.coeffs)
309
323
 
310
324
  # Reporting error
311
- P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
325
+ P_res = numpy.abs(eval_P(z_fit, m1_fit, self.coeffs))
312
326
  res_max = numpy.max(P_res[numpy.isfinite(P_res)])
313
327
  res_99_9 = numpy.quantile(P_res[numpy.isfinite(P_res)], 0.999)
314
328
 
315
329
  # Check polynomial has Stieltjes root
316
330
  x_min = self.lam_m - 1.0
317
331
  x_max = self.lam_p + 1.0
318
- status = sanity_check_stieltjes_branch(a_coeffs, x_min, x_max,
332
+ status = sanity_check_stieltjes_branch(self.coeffs, x_min, x_max,
319
333
  eta=max(y_eps, 1e-2), n_x=128,
320
334
  max_bad_frac=0.05)
321
335
 
@@ -327,7 +341,7 @@ class AlgebraicForm(object):
327
341
  # -----------------
328
342
 
329
343
  # Inflate a bit to make sure all points are searched
330
- # x_min, x_max = self._inflate_broad_support(inflate=0.2)
344
+ # x_min, x_max = self._inflate_broad_supp(inflate=0.2)
331
345
  # scale = float(max(1.0, abs(x_max - x_min), abs(x_min), abs(x_max)))
332
346
  # eta = 1e-6 * scale
333
347
  #
@@ -340,10 +354,10 @@ class AlgebraicForm(object):
340
354
  # }
341
355
 
342
356
  # NOTE overwrite init
343
- self._stieltjes = StieltjesPoly(self.a_coeffs)
344
- # self._stieltjes = StieltjesPoly(self.a_coeffs, viterbi_opt=vopt)
357
+ self._stieltjes = StieltjesPoly(self.coeffs)
358
+ # self._stieltjes = StieltjesPoly(self.coeffs, viterbi_opt=vopt)
345
359
 
346
- self._moments_base = AlgebraicStieltjesMoments(a_coeffs)
360
+ self._moments_base = AlgebraicStieltjesMoments(self.coeffs)
347
361
  self.moments = Moments(self._moments_base)
348
362
 
349
363
  if verbose:
@@ -352,14 +366,14 @@ class AlgebraicForm(object):
352
366
 
353
367
  print('\nCoefficients (real)')
354
368
  with numpy.printoptions(precision=8, suppress=True):
355
- for i in range(a_coeffs.shape[0]):
356
- for j in range(a_coeffs.shape[1]):
357
- v = a_coeffs[i, j]
369
+ for i in range(self.coeffs.shape[0]):
370
+ for j in range(self.coeffs.shape[1]):
371
+ v = self.coeffs[i, j]
358
372
  print(f'{v.real:>+0.8f}', end=' ')
359
373
  print('')
360
374
 
361
- a_coeffs_img_norm = numpy.linalg.norm(a_coeffs.imag, ord='fro')
362
- print(f'\nCoefficients (imag) norm: {a_coeffs_img_norm:>0.4e}')
375
+ coeffs_img_norm = numpy.linalg.norm(self.coeffs.imag, ord='fro')
376
+ print(f'\nCoefficients (imag) norm: {coeffs_img_norm:>0.4e}')
363
377
 
364
378
  if not status['ok']:
365
379
  print("\nWARNING: sanity check failed:\n" +
@@ -369,17 +383,22 @@ class AlgebraicForm(object):
369
383
  else:
370
384
  print('\nStieltjes sanity check: OK')
371
385
 
372
- return a_coeffs, self.est_support, status
386
+ return self.coeffs, self.est_supp, status
373
387
 
374
- # =====================
375
- # inflate broad support
376
- # =====================
388
+ # ==================
389
+ # inflate broad supp
390
+ # ==================
377
391
 
378
- def _inflate_broad_support(self, inflate=0.0):
392
+ def _inflate_broad_supp(self, inflate=0.0):
379
393
  """
394
+ Inflate the broad support for better post-processing, such as detecting
395
+ branch points, spectral edges, etc.
380
396
  """
381
397
 
382
- min_supp, max_supp = self.broad_support
398
+ if inflate < 0:
399
+ raise ValueError('"inflate" should be non-negative.')
400
+
401
+ min_supp, max_supp = self.broad_supp
383
402
 
384
403
  c_supp = 0.5 * (max_supp + min_supp)
385
404
  r_supp = 0.5 * (max_supp - min_supp)
@@ -389,68 +408,48 @@ class AlgebraicForm(object):
389
408
 
390
409
  return x_min, x_max
391
410
 
392
- # ================
393
- # estimate support
394
- # ================
411
+ # =======
412
+ # support
413
+ # =======
395
414
 
396
- def estimate_support(self, a_coeffs=None, scan_range=None, n_scan=4000):
415
+ def support(self, coeffs=None, scan_range=None, n_scan=4000):
397
416
  """
417
+ Estimate the spectral edges of the density.
398
418
  """
399
419
 
400
- if a_coeffs is None:
401
- if self.a_coeffs is None:
420
+ if coeffs is None:
421
+ if self.coeffs is None:
402
422
  raise RuntimeError('Call "fit" first.')
403
423
  else:
404
- a_coeffs = self.a_coeffs
424
+ coeffs = self.coeffs
405
425
 
406
426
  # Inflate a bit to make sure all points are searched
407
427
  if scan_range is not None:
408
428
  x_min, x_max = scan_range
409
429
  else:
410
- x_min, x_max = self._inflate_broad_support(inflate=0.2)
411
-
412
- est_support, info = compute_support(a_coeffs, x_min=x_min, x_max=x_max,
413
- n_scan=n_scan)
414
-
415
- return est_support, info
416
-
417
- # ======================
418
- # estimate branch points
419
- # ======================
430
+ x_min, x_max = self._inflate_broad_supp(inflate=0.2)
420
431
 
421
- def estimate_branch_points(self, tol=1e-15, real_tol=None):
422
- """
423
- Compute global branch points and zeros of leading a_j
424
- """
425
-
426
- if self.a_coeffs is None:
427
- raise RuntimeError('Call "fit" first.')
432
+ est_supp, info = estimate_support(coeffs, x_min=x_min, x_max=x_max,
433
+ n_scan=n_scan)
428
434
 
429
- bp, leading_zeros, info = compute_branch_points(
430
- self.a_coeffs, tol=tol, real_tol=real_tol)
431
-
432
- return bp, leading_zeros, info
435
+ return est_supp, info
433
436
 
434
437
  # =============
435
- # generate grid
438
+ # branch points
436
439
  # =============
437
440
 
438
- def _generate_grid(self, scale, extend=1.0, N=500):
441
+ def branch_points(self, tol=1e-15, real_tol=None):
439
442
  """
440
- Generate a grid of points to evaluate density / Hilbert / Stieltjes
441
- transforms.
443
+ Compute global branch points and zeros of leading coefficient.
442
444
  """
443
445
 
444
- radius = 0.5 * (self.lam_p - self.lam_m)
445
- center = 0.5 * (self.lam_p + self.lam_m)
446
-
447
- x_min = numpy.floor(extend * (center - extend * radius * scale))
448
- x_max = numpy.ceil(extend * (center + extend * radius * scale))
446
+ if self.coeffs is None:
447
+ raise RuntimeError('Call "fit" first.')
449
448
 
450
- x_min /= extend
451
- x_max /= extend
449
+ bp, leading_zeros, info = estimate_branch_points(
450
+ self.coeffs, tol=tol, real_tol=real_tol)
452
451
 
453
- return numpy.linspace(x_min, x_max, N)
452
+ return bp, leading_zeros, info
454
453
 
455
454
  # =======
456
455
  # density
@@ -498,7 +497,7 @@ class AlgebraicForm(object):
498
497
  >>> from freealg import FreeForm
499
498
  """
500
499
 
501
- if self.a_coeffs is None:
500
+ if self.coeffs is None:
502
501
  raise RuntimeError('The model needs to be fit using the .fit() ' +
503
502
  'function.')
504
503
 
@@ -511,7 +510,7 @@ class AlgebraicForm(object):
511
510
  rho = self._stieltjes(z).imag / numpy.pi
512
511
 
513
512
  if plot:
514
- plot_density(x, rho, eig=self.eig, support=self.broad_support,
513
+ plot_density(x, rho, eig=self.eig, support=self.broad_supp,
515
514
  label='Estimate', latex=latex, save=save)
516
515
 
517
516
  return rho
@@ -563,7 +562,7 @@ class AlgebraicForm(object):
563
562
  >>> from freealg import FreeForm
564
563
  """
565
564
 
566
- if self.a_coeffs is None:
565
+ if self.coeffs is None:
567
566
  raise RuntimeError('The model needs to be fit using the .fit() ' +
568
567
  'function.')
569
568
 
@@ -575,7 +574,7 @@ class AlgebraicForm(object):
575
574
  hilb = -self._stieltjes(x).real / numpy.pi
576
575
 
577
576
  if plot:
578
- plot_hilbert(x, hilb, support=self.broad_support, latex=latex,
577
+ plot_hilbert(x, hilb, support=self.broad_supp, latex=latex,
579
578
  save=save)
580
579
 
581
580
  return hilb
@@ -586,7 +585,7 @@ class AlgebraicForm(object):
586
585
 
587
586
  def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
588
587
  """
589
- Compute Stieltjes transform of the spectral density on a grid.
588
+ Compute Stieltjes transform of the spectral density
590
589
 
591
590
  This function evaluates Stieltjes transform on an array of points, or
592
591
  over a 2D Cartesian grid on the complex plane.
@@ -635,7 +634,7 @@ class AlgebraicForm(object):
635
634
  >>> from freealg import FreeForm
636
635
  """
637
636
 
638
- if self.a_coeffs is None:
637
+ if self.coeffs is None:
639
638
  raise RuntimeError('The model needs to be fit using the .fit() ' +
640
639
  'function.')
641
640
 
@@ -659,44 +658,11 @@ class AlgebraicForm(object):
659
658
  m = self._stieltjes(z, progress=True)
660
659
 
661
660
  if plot:
662
- plot_stieltjes(x, y, m, m, self.broad_support, latex=latex,
661
+ plot_stieltjes(x, y, m, m, self.broad_supp, latex=latex,
663
662
  save=save)
664
663
 
665
664
  return m
666
665
 
667
- # ==============
668
- # eval stieltjes
669
- # ==============
670
-
671
- def _eval_stieltjes(self, z, branches=False):
672
- """
673
- Compute Stieltjes transform of the spectral density.
674
-
675
- Parameters
676
- ----------
677
-
678
- z : numpy.array
679
- The z values in the complex plan where the Stieltjes transform is
680
- evaluated.
681
-
682
- branches : bool, default = False
683
- Return both the principal and secondary branches of the Stieltjes
684
- transform. The default ``branches=False`` will return only
685
- the secondary branch.
686
-
687
- Returns
688
- -------
689
-
690
- m_p : numpy.ndarray
691
- The Stieltjes transform on the principal branch if
692
- ``branches=True``.
693
-
694
- m_m : numpy.ndarray
695
- The Stieltjes transform continued to the secondary branch.
696
- """
697
-
698
- pass
699
-
700
666
  # ==========
701
667
  # decompress
702
668
  # ==========
@@ -756,7 +722,7 @@ class AlgebraicForm(object):
756
722
 
757
723
  # Evolve
758
724
  W, ok = decompress_newton(
759
- z_query, t_all, self.a_coeffs,
725
+ z_query, t_all, self.coeffs,
760
726
  w0_list=w0_list, **newton_opt)
761
727
 
762
728
  rho_all = W.imag / numpy.pi
@@ -775,7 +741,7 @@ class AlgebraicForm(object):
775
741
  # Decompress to each alpha
776
742
  for i in range(alpha.size):
777
743
  t_i = numpy.log(alpha[i])
778
- coeffs_i = decompress_coeffs(self.a_coeffs, t_i)
744
+ coeffs_i = decompress_coeffs(self.coeffs, t_i)
779
745
 
780
746
  def mom(k):
781
747
  return self.moments(k, t_i)
@@ -808,6 +774,9 @@ class AlgebraicForm(object):
808
774
  # ==========
809
775
 
810
776
  def candidates(self, size, x=None, verbose=False):
777
+ """
778
+ Candidate densities of free decompression from all possible roots
779
+ """
811
780
 
812
781
  # Check size argument
813
782
  if numpy.isscalar(size):
@@ -842,410 +811,235 @@ class AlgebraicForm(object):
842
811
 
843
812
  for i in range(alpha.size):
844
813
  t_i = numpy.log(alpha[i])
845
- coeffs_i = decompress_coeffs(self.a_coeffs, t_i)
814
+ coeffs_i = decompress_coeffs(self.coeffs, t_i)
846
815
  plot_candidates(coeffs_i, x, size=int(alpha[i]*self.n),
847
816
  verbose=verbose)
848
817
 
849
- # ====
850
- # edge
851
- # ====
852
-
853
- def edge(self, t, eta=1e-3, dt_max=0.1, max_iter=30, tol=1e-12,
854
- verbose=False):
855
- """
856
- Evolves spectral edges.
818
+ # =================
819
+ # is decompressible
820
+ # =================
857
821
 
858
- Fix: if t is a scalar or length-1 array, we prepend t=0 internally so
859
- evolve_edges actually advances from the initialization at t=0.
822
+ def is_decompressible(self, ratio=2, n_ratios=5, K=(6, 8, 10), L=3,
823
+ tol=1e-8, verbose=False):
860
824
  """
825
+ Check if the given distribution can be decompressed.
861
826
 
862
- if self.support is not None:
863
- known_support = self.support
864
- elif self.est_support is not None:
865
- known_support = self.est_support
866
- else:
867
- raise RuntimeError('Call "fit" first.')
868
-
869
- t = numpy.asarray(t, dtype=float).ravel()
827
+ To this end, this function checks if the evolved polynomial under the
828
+ free decompression admits a valid Stieltjes root.
870
829
 
871
- if t.size == 1:
872
- t1 = float(t[0])
873
- if t1 == 0.0:
874
- t_grid = numpy.array([0.0], dtype=float)
875
- complex_edges, ok_edges = evolve_edges(
876
- t_grid, self.a_coeffs, support=known_support, eta=eta,
877
- dt_max=dt_max, max_iter=max_iter, tol=tol
878
- )
879
- else:
880
- # prepend 0 and drop it after evolution
881
- t_grid = numpy.array([0.0, t1], dtype=float)
882
- complex_edges2, ok_edges2 = evolve_edges(
883
- t_grid, self.a_coeffs, support=known_support, eta=eta,
884
- dt_max=dt_max, max_iter=max_iter, tol=tol
885
- )
886
- complex_edges = complex_edges2[-1:, :]
887
- ok_edges = ok_edges2[-1:, :]
888
- else:
889
- # For vector t, require it starts at 0 for correct initialization
890
- # (you can relax this if you want by prepending 0 similarly).
891
- complex_edges, ok_edges = evolve_edges(
892
- t, self.a_coeffs, support=known_support, eta=eta,
893
- dt_max=dt_max, max_iter=max_iter, tol=tol
894
- )
830
+ Parameters
831
+ ----------
895
832
 
896
- real_edges = complex_edges.real
833
+ ratio : float, default=2
834
+ The maximum ratio of decompressed matrix size to the original size.
897
835
 
898
- # Remove spurious edges / merges for plotting
899
- real_merged_edges, active_k = merge_edges(real_edges, tol=1e-4)
836
+ n_ratios : int, default=5
837
+ Number of ratios to test from 1 (no decompression) to the given
838
+ maximum ``ratio``.
900
839
 
901
- if verbose:
902
- print("edge success rate:", ok_edges.mean())
840
+ K : sequence of int, default=(6, 8, 10)
841
+ Truncation orders ``K`` used to build the Laurent cancellation
842
+ system. Each ``K`` enforces powers ``p`` in ``[-L, ..., K]``.
903
843
 
904
- return complex_edges, real_merged_edges, active_k
844
+ L : int, default=3
845
+ Number of negative powers to enforce. The enforced power range is
846
+ ``p in [-L, ..., K]`` for each ``K``.
905
847
 
906
- # ====
907
- # cusp
908
- # ====
848
+ tol : float, default=1e-10
849
+ Pass threshold for the best residual over ``K``. The residual
850
+ is ``max_p |c_p|`` over enforced powers, where ``c_p`` are the
851
+ Laurent coefficients of :math:`P_t(1/w, m(w))`.
909
852
 
910
- def cusp(self, t_grid):
911
- """
912
- """
853
+ verbose : bool, default=True
854
+ If True, print a per-``t`` summary and the per-``K`` diagnostics.
913
855
 
914
- return cusp_wrap(self, t_grid, edge_kwargs=None, max_iter=50,
915
- tol=1.0e-12)
916
-
917
- # ========
918
- # eigvalsh
919
- # ========
920
-
921
- def eigvalsh(self, size=None, seed=None, **kwargs):
922
- """
923
- Estimate the eigenvalues.
924
-
925
- This function estimates the eigenvalues of the freeform matrix
926
- or a larger matrix containing it using free decompression.
927
-
928
- Parameters
929
- ----------
856
+ Returns
857
+ -------
930
858
 
931
- size : int, default=None
932
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
933
- eigenvalues of. If None, returns estimates of the eigenvalues of
934
- :math:`\\mathbf{A}` itself.
859
+ status : array
860
+ Boolean array of `True` or `False for each time in ``t``.
861
+ `True` means decompressible, and `False` means not decompressible.
935
862
 
936
- seed : int, default=None
937
- The seed for the Quasi-Monte Carlo sampler.
863
+ info : dict
864
+ Dictionary with the following keys
938
865
 
939
- **kwargs : dict, optional
940
- Pass additional options to the underlying
941
- :func:`FreeForm.decompress` function.
866
+ * ``'ratios'``: List of decompression ratios that is checked.
867
+ * ``'ok'``: status of the decompressiblity at the tested ratio.
868
+ * ``'res'``: details of test for each ratio.
942
869
 
943
- Returns
944
- -------
870
+ Raises
871
+ ------
945
872
 
946
- eigs : numpy.array
947
- Eigenvalues of decompressed matrix
873
+ ValueError
874
+ If ``K`` is empty, or if any ``K`` or ``L`` is not positive.
948
875
 
949
876
  See Also
950
877
  --------
951
878
 
952
- FreeForm.decompress
953
- FreeForm.cond
879
+ branch_points :
880
+ Geometric branch-point estimation; complementary to this asymptotic
881
+ check.
954
882
 
955
883
  Notes
956
884
  -----
957
885
 
958
- All arguments to the `.decompress()` procedure can be provided.
886
+ This is a ""no-FD-run" diagnostic: it only uses the base algebraic
887
+ relation :math:`P(z,m)=0` (stored in ``self.coeffs``) and tests the
888
+ pushed relation under free decompression (FD) at selected expansion
889
+ factors ``t``.
959
890
 
960
- Examples
961
- --------
891
+ The FD pushforward used here is the characteristic change-of-variables
962
892
 
963
- .. code-block:: python
964
- :emphasize-lines: 1
893
+ * :math:`\\tau = e^t`
894
+ * :math:`y = \\tau * m`
895
+ * :math:`\\zeta = z + (1 - 1/\\tau) / m`
896
+ * :math:`P_t(z, m) = P(\\zeta, y)`
965
897
 
966
- >>> from freealg import FreeForm
967
- """
968
-
969
- # if size is None:
970
- # size = self.n
971
- #
972
- # rho, x = self.decompress(size, **kwargs)
973
- # eigs = numpy.sort(sample(x, rho, size, method='qmc', seed=seed))
974
- #
975
- # return eigs
976
- pass
977
-
978
- # ====
979
- # cond
980
- # ====
898
+ A necessary condition for FD tracking to remain well-posed is that, for
899
+ each :math:`\\tau > 1`), the pushed relation admits a "Stieltjes branch
900
+ at infinity", i.e., a solution branch with the large
901
+ :math:`\\vert z \\vert` behavior
981
902
 
982
- def cond(self, size=None, seed=None, **kwargs):
983
- """
984
- Estimate the condition number.
903
+ .. math::
985
904
 
986
- This function estimates the condition number of the matrix
987
- :math:`\\mathbf{A}` or a larger matrix containing :math:`\\mathbf{A}`
988
- using free decompression.
905
+ m(z) = -1/z + O(1/z^2)
989
906
 
990
- Parameters
991
- ----------
992
-
993
- size : int, default=None
994
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
995
- eigenvalues of. If None, returns estimates of the eigenvalues of
996
- :math:`\\mathbf{A}` itself.
907
+ as :math:`\\vert z \\vert \\to \\infty` in :math:`\\Im(z) > 0`.
997
908
 
998
- **kwargs : dict, optional
999
- Pass additional options to the underlying
1000
- :func:`FreeForm.decompress` function.
909
+ This routine enforces that asymptotic structure by constructing a
910
+ truncated Laurent expansion in :math:`w = 1/z`. It solves for
911
+ coefficients in
1001
912
 
1002
- Returns
1003
- -------
913
+ .. math::
1004
914
 
1005
- c : float
1006
- Condition number
915
+ m(z) = -(alpha/z + mu_1/z^2 + mu_2/z^3 + ...)
1007
916
 
1008
- See Also
1009
- --------
917
+ so that the Laurent series of :math:`P_t(1/w, m(w))` cancels on a
918
+ prescribed range of powers. Concretely, for each truncation order
919
+ ``K``, we enforce the cancellation of powers ``p`` in ``[-L, ..., K]``
920
+ and measure the maximum absolute residual among those enforced
921
+ coefficients. The best (smallest) residual across ``K`` is used for the
922
+ main pass/fail decision.
1010
923
 
1011
- FreeForm.eigvalsh
1012
- FreeForm.norm
1013
- FreeForm.slogdet
1014
- FreeForm.trace
924
+ * ``K`` controls how many asymptotic constraints are enforced. Larger
925
+ ``K`` is usually stricter but can become sensitive to coefficient
926
+ noise (e.g. from a fitted polynomial).
927
+ * ``L`` controls how many negative powers are enforced. A safe default
928
+ is ``deg_z + 2``; here we expose it directly so you can tune it per
929
+ model.
1015
930
 
1016
931
  Examples
1017
932
  --------
1018
933
 
1019
934
  .. code-block:: python
1020
- :emphasize-lines: 1
935
+ :emphasize-lines: 9,10
1021
936
 
1022
- >>> from freealg import FreeForm
1023
- """
937
+ >>> import freealg AlgebraicForm
938
+ >>> from freealg.distributions import CompoundPoisson
1024
939
 
1025
- eigs = self.eigvalsh(size=size, **kwargs)
1026
- return eigs.max() / eigs.min()
940
+ >>> # Create compound free Poisson law
941
+ >>> cp = CompoundPoisson(t1=2.0, t2=5.5, w1=0.75, c=0.1)
942
+ >>> af = AlgebraicForm(cp)
1027
943
 
1028
- # =====
1029
- # trace
1030
- # =====
944
+ >>> # Check the decompressibility of compound free Poisson
945
+ >>> status, info = af.is_decompressible(ratio=2, n_ratios=5,
946
+ ... verbose=True)
1031
947
 
1032
- def trace(self, size=None, p=1.0, seed=None, **kwargs):
948
+ >>> status
949
+ True
1033
950
  """
1034
- Estimate the trace of a power.
1035
951
 
1036
- This function estimates the trace of the matrix power
1037
- :math:`\\mathbf{A}^p` of the freeform or that of a larger matrix
1038
- containing it.
952
+ if self.coeffs is None:
953
+ raise RuntimeError('"fit" model first.')
1039
954
 
1040
- Parameters
1041
- ----------
1042
-
1043
- size : int, default=None
1044
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1045
- eigenvalues of. If None, returns estimates of the eigenvalues of
1046
- :math:`\\mathbf{A}` itself.
1047
-
1048
- p : float, default=1.0
1049
- The exponent :math:`p` in :math:`\\mathbf{A}^p`.
1050
-
1051
- seed : int, default=None
1052
- The seed for the Quasi-Monte Carlo sampler.
1053
-
1054
- **kwargs : dict, optional
1055
- Pass additional options to the underlying
1056
- :func:`FreeForm.decompress` function.
1057
-
1058
- Returns
1059
- -------
955
+ if ratio < 1:
956
+ raise ValueError('"ratio" cannot be smaller than 1.')
1060
957
 
1061
- trace : float
1062
- matrix trace
958
+ tau = numpy.linspace(0.0, ratio, n_ratios)
959
+ ok = numpy.zeros_like(tau, dtype=bool)
960
+ res = [] * tau.size
1063
961
 
1064
- See Also
1065
- --------
962
+ for i in range(tau.size):
963
+ ok[i], res[i] = precheck_laurent(self.coeffs, tau[i], K=K, L=L,
964
+ tol=tol, verbose=verbose)
1066
965
 
1067
- FreeForm.eigvalsh
1068
- FreeForm.cond
1069
- FreeForm.slogdet
1070
- FreeForm.norm
966
+ if verbose:
967
+ print("")
1071
968
 
1072
- Notes
1073
- -----
969
+ status = numpy.any(numpy.logical_not(ok))
1074
970
 
1075
- The trace is highly amenable to subsampling: under free decompression
1076
- the average eigenvalue is assumed constant, so the trace increases
1077
- linearly. Traces of powers fall back to :func:`eigvalsh`.
1078
- All arguments to the `.decompress()` procedure can be provided.
971
+ info = {
972
+ 'ratios': tau,
973
+ 'ok': ok,
974
+ 'res': res,
975
+ }
1079
976
 
1080
- Examples
1081
- --------
977
+ return status, info
1082
978
 
1083
- .. code-block:: python
1084
- :emphasize-lines: 1
979
+ # ====
980
+ # edge
981
+ # ====
1085
982
 
1086
- >>> from freealg import FreeForm
983
+ def edge(self, t, eta=1e-3, dt_max=0.1, max_iter=30, tol=1e-12,
984
+ verbose=False):
1087
985
  """
986
+ Evolves spectral edges.
1088
987
 
1089
- if numpy.isclose(p, 1.0):
1090
- return numpy.mean(self.eig) * (size / self.n)
1091
-
1092
- eig = self.eigvalsh(size=size, seed=seed, **kwargs)
1093
- return numpy.sum(eig ** p)
1094
-
1095
- # =======
1096
- # slogdet
1097
- # =======
1098
-
1099
- def slogdet(self, size=None, seed=None, **kwargs):
988
+ Fix: if t is a scalar or length-1 array, we prepend t=0 internally so
989
+ evolve_edges actually advances from the initialization at t=0.
1100
990
  """
1101
- Estimate the sign and logarithm of the determinant.
1102
-
1103
- This function estimates the *slogdet* of the freeform or that of
1104
- a larger matrix containing it using free decompression.
1105
991
 
1106
- Parameters
1107
- ----------
1108
-
1109
- size : int, default=None
1110
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1111
- eigenvalues of. If None, returns estimates of the eigenvalues of
1112
- :math:`\\mathbf{A}` itself.
1113
-
1114
- seed : int, default=None
1115
- The seed for the Quasi-Monte Carlo sampler.
1116
-
1117
- Returns
1118
- -------
1119
-
1120
- sign : float
1121
- Sign of determinant
1122
-
1123
- ld : float
1124
- natural logarithm of the absolute value of the determinant
1125
-
1126
- See Also
1127
- --------
992
+ if self.supp is not None:
993
+ known_supp = self.supp
994
+ elif self.est_supp is not None:
995
+ known_supp = self.est_supp
996
+ else:
997
+ raise RuntimeError('Call "fit" first.')
1128
998
 
1129
- FreeForm.eigvalsh
1130
- FreeForm.cond
1131
- FreeForm.trace
1132
- FreeForm.norm
999
+ t = numpy.asarray(t, dtype=float).ravel()
1133
1000
 
1134
- Notes
1135
- -----
1001
+ if t.size == 1:
1002
+ t1 = float(t[0])
1003
+ if t1 == 0.0:
1004
+ t_grid = numpy.array([0.0], dtype=float)
1005
+ complex_edges, ok_edges = evolve_edges(
1006
+ t_grid, self.coeffs, support=known_supp, eta=eta,
1007
+ dt_max=dt_max, max_iter=max_iter, tol=tol
1008
+ )
1009
+ else:
1010
+ # prepend 0 and drop it after evolution
1011
+ t_grid = numpy.array([0.0, t1], dtype=float)
1012
+ complex_edges2, ok_edges2 = evolve_edges(
1013
+ t_grid, self.coeffs, support=known_supp, eta=eta,
1014
+ dt_max=dt_max, max_iter=max_iter, tol=tol)
1136
1015
 
1137
- All arguments to the `.decompress()` procedure can be provided.
1016
+ complex_edges = complex_edges2[-1:, :]
1017
+ ok_edges = ok_edges2[-1:, :]
1018
+ else:
1019
+ # For vector t, require it starts at 0 for correct initialization
1020
+ # (you can relax this if you want by prepending 0 similarly).
1021
+ complex_edges, ok_edges = evolve_edges(
1022
+ t, self.coeffs, support=known_supp, eta=eta,
1023
+ dt_max=dt_max, max_iter=max_iter, tol=tol)
1138
1024
 
1139
- Examples
1140
- --------
1025
+ real_edges = complex_edges.real
1141
1026
 
1142
- .. code-block:: python
1143
- :emphasize-lines: 1
1027
+ # Remove spurious edges / merges for plotting
1028
+ real_merged_edges, active_k = merge_edges(real_edges, tol=1e-4)
1144
1029
 
1145
- >>> from freealg import FreeForm
1146
- """
1030
+ if verbose:
1031
+ print("edge success rate:", ok_edges.mean())
1147
1032
 
1148
- eigs = self.eigvalsh(size=size, seed=seed, **kwargs)
1149
- sign = numpy.prod(numpy.sign(eigs))
1150
- ld = numpy.sum(numpy.log(numpy.abs(eigs)))
1151
- return sign, ld
1033
+ return complex_edges, real_merged_edges, active_k
1152
1034
 
1153
1035
  # ====
1154
- # norm
1036
+ # cusp
1155
1037
  # ====
1156
1038
 
1157
- def norm(self, size=None, order=2, seed=None, **kwargs):
1039
+ def cusp(self, t_grid):
1158
1040
  """
1159
- Estimate the Schatten norm.
1160
-
1161
- This function estimates the norm of the freeform or a larger
1162
- matrix containing it using free decompression.
1163
-
1164
- Parameters
1165
- ----------
1166
-
1167
- size : int, default=None
1168
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1169
- eigenvalues of. If None, returns estimates of the eigenvalues of
1170
- :math:`\\mathbf{A}` itself.
1171
-
1172
- order : {float, ``''inf``, ``'-inf'``, ``'fro'``, ``'nuc'``}, default=2
1173
- Order of the norm.
1174
-
1175
- * float :math:`p`: Schatten p-norm.
1176
- * ``'inf'``: Largest absolute eigenvalue
1177
- :math:`\\max \\vert \\lambda_i \\vert)`
1178
- * ``'-inf'``: Smallest absolute eigenvalue
1179
- :math:`\\min \\vert \\lambda_i \\vert)`
1180
- * ``'fro'``: Frobenius norm corresponding to :math:`p=2`
1181
- * ``'nuc'``: Nuclear (or trace) norm corresponding to :math:`p=1`
1182
-
1183
- seed : int, default=None
1184
- The seed for the Quasi-Monte Carlo sampler.
1185
-
1186
- **kwargs : dict, optional
1187
- Pass additional options to the underlying
1188
- :func:`FreeForm.decompress` function.
1189
-
1190
- Returns
1191
- -------
1192
-
1193
- norm : float
1194
- matrix norm
1195
-
1196
- See Also
1197
- --------
1198
-
1199
- FreeForm.eigvalsh
1200
- FreeForm.cond
1201
- FreeForm.slogdet
1202
- FreeForm.trace
1203
-
1204
- Notes
1205
- -----
1206
-
1207
- Thes Schatten :math:`p`-norm is defined by
1208
-
1209
- .. math::
1210
-
1211
- \\Vert \\mathbf{A} \\Vert_p = \\left(
1212
- \\sum_{i=1}^N \\vert \\lambda_i \\vert^p \\right)^{1/p}.
1213
-
1214
- Examples
1215
- --------
1216
-
1217
- .. code-block:: python
1218
- :emphasize-lines: 1
1219
-
1220
- >>> from freealg import FreeForm
1041
+ Find cusp (merge) point of evolving spectral edges
1221
1042
  """
1222
1043
 
1223
- eigs = self.eigvalsh(size, seed=seed, **kwargs)
1224
-
1225
- # Check order type and convert to float
1226
- if order == 'nuc':
1227
- order = 1
1228
- elif order == 'fro':
1229
- order = 2
1230
- elif order == 'inf':
1231
- order = float('inf')
1232
- elif order == '-inf':
1233
- order = -float('inf')
1234
- elif not isinstance(order,
1235
- (int, float, numpy.integer, numpy.floating)) \
1236
- and not isinstance(order, (bool, numpy.bool_)):
1237
- raise ValueError('"order" is invalid.')
1238
-
1239
- # Compute norm
1240
- if numpy.isinf(order) and not numpy.isneginf(order):
1241
- norm_ = max(numpy.abs(eigs))
1242
-
1243
- elif numpy.isneginf(order):
1244
- norm_ = min(numpy.abs(eigs))
1245
-
1246
- elif isinstance(order, (int, float, numpy.integer, numpy.floating)) \
1247
- and not isinstance(order, (bool, numpy.bool_)):
1248
- norm_q = numpy.sum(numpy.abs(eigs)**order)
1249
- norm_ = norm_q**(1.0 / order)
1250
-
1251
- return norm_
1044
+ return cusp_wrap(self, t_grid, edge_kwargs=None, max_iter=50,
1045
+ tol=1.0e-12)