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,386 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the license found in the LICENSE.txt file in the root
7
+ # directory of this source tree.
8
+
9
+
10
+ # =======
11
+ # Imports
12
+ # =======
13
+
14
+ import numpy
15
+ from .._algebraic_form._sheets_util import _pick_physical_root_scalar
16
+
17
+ __all__ = ['DeformedWigner']
18
+
19
+
20
+ # ===============
21
+ # Deformed Wigner
22
+ # ===============
23
+
24
+ class DeformedWigner(object):
25
+ """
26
+ Deformed Wiger
27
+ """
28
+
29
+ # ====
30
+ # init
31
+ # ====
32
+
33
+ def __init__(self, t1, t2, w1, sigma=1.0):
34
+ """
35
+ Initialization.
36
+ """
37
+
38
+ if not (0.0 <= w1 <= 1.0):
39
+ raise ValueError("w1 must be in [0, 1].")
40
+
41
+ self.t1 = t1
42
+ self.t2 = t2
43
+ self.w1 = w1
44
+ self.sigma = sigma
45
+
46
+ # ==================
47
+ # roots cubic scalar
48
+ # ==================
49
+
50
+ def _roots_cubic_scalar(self, z):
51
+ """
52
+ """
53
+
54
+ # Unpack parameters
55
+ t1 = self.t1
56
+ t2 = self.t2
57
+ w1 = self.w1
58
+ sigma = self.sigma
59
+
60
+ w2 = 1.0 - w1
61
+ s2 = sigma * sigma
62
+ a1 = t1 - z
63
+ a2 = t2 - z
64
+
65
+ c3 = s2 * s2
66
+ c2 = -s2 * (a1 + a2)
67
+ c1 = (a1 * a2) + s2
68
+ c0 = -(w1 * a2 + w2 * a1)
69
+
70
+ return numpy.roots([c3, c2, c1, c0])
71
+
72
+ # =========
73
+ # stieltjes
74
+ # =========
75
+
76
+ def stieltjes(self, z, max_iter=100, tol=1e-12):
77
+ """
78
+ """
79
+
80
+ # Unpack parameters
81
+ t1 = self.t1
82
+ t2 = self.t2
83
+ w1 = self.w1
84
+ sigma = self.sigma
85
+
86
+ w2 = 1.0 - w1
87
+ s2 = sigma * sigma
88
+
89
+ z = numpy.asarray(z, dtype=numpy.complex128)
90
+ scalar = (z.ndim == 0)
91
+ if scalar:
92
+ z = z.reshape((1,))
93
+
94
+ m = -1.0 / z
95
+ active = numpy.isfinite(m)
96
+
97
+ for _ in range(int(max_iter)):
98
+ if not numpy.any(active):
99
+ break
100
+
101
+ ma = m[active]
102
+ za = z[active]
103
+
104
+ d1 = (t1 - za - s2 * ma)
105
+ d2 = (t2 - za - s2 * ma)
106
+
107
+ f = ma - (w1 / d1 + w2 / d2)
108
+ fp = 1.0 - (w1 * s2 / (d1 * d1) + w2 * s2 / (d2 * d2))
109
+
110
+ step = f / fp
111
+ ma2 = ma - step
112
+ m[active] = ma2
113
+
114
+ conv = numpy.abs(step) < tol * (1.0 + numpy.abs(ma2))
115
+ idx = numpy.where(active)[0]
116
+ active[idx[conv]] = False
117
+
118
+ sign = numpy.where(numpy.imag(z) >= 0.0, 1.0, -1.0)
119
+ bad = (sign * numpy.imag(m) <= 0.0) | (~numpy.isfinite(m))
120
+
121
+ if numpy.any(bad):
122
+ zf = z.ravel()
123
+ mf = m.ravel()
124
+ bad_idx = numpy.where(bad.ravel())[0]
125
+ for i in bad_idx:
126
+ r = self._roots_cubic_scalar(zf[i])
127
+ mf[i] = _pick_physical_root_scalar(zf[i], r)
128
+ m = mf.reshape(z.shape)
129
+
130
+ if scalar:
131
+ return m.reshape(())
132
+ return m
133
+
134
+ # =======
135
+ # density
136
+ # =======
137
+
138
+ def density(self, x, eta=1e-3):
139
+ """
140
+ """
141
+
142
+ # Unpack parameters
143
+ t1 = self.t1
144
+ t2 = self.t2
145
+ w1 = self.w1
146
+ sigma = self.sigma
147
+
148
+ x = numpy.asarray(x, dtype=numpy.float64)
149
+ z = x + 1j * float(eta)
150
+
151
+ zf = z.ravel()
152
+ m = numpy.empty_like(zf, dtype=numpy.complex128)
153
+
154
+ m_prev = None
155
+ for i in range(zf.size):
156
+ zi = zf[i]
157
+ if m_prev is None:
158
+ mi = -1.0 / zi
159
+ else:
160
+ mi = complex(m_prev)
161
+
162
+ for _ in range(80):
163
+ d1 = (t1 - zi - (sigma * sigma) * mi)
164
+ d2 = (t2 - zi - (sigma * sigma) * mi)
165
+
166
+ f = mi - (w1 / d1 + (1.0 - w1) / d2)
167
+ fp = 1.0 - (
168
+ w1 * (sigma * sigma) / (d1 * d1) +
169
+ (1.0 - w1) * (sigma * sigma) / (d2 * d2)
170
+ )
171
+
172
+ step = f / fp
173
+ mi2 = mi - step
174
+ if abs(step) < 1e-12 * (1.0 + abs(mi2)):
175
+ mi = mi2
176
+ break
177
+ mi = mi2
178
+
179
+ m[i] = mi
180
+ m_prev = mi
181
+
182
+ m = m.reshape(z.shape)
183
+ rho = numpy.imag(m) / numpy.pi
184
+ rho = numpy.maximum(rho, 0.0)
185
+ return rho
186
+
187
+ # =====
188
+ # roots
189
+ # =====
190
+
191
+ def roots(self, z):
192
+ """
193
+ """
194
+
195
+ z = numpy.asarray(z, dtype=numpy.complex128)
196
+ scalar = (z.ndim == 0)
197
+ if scalar:
198
+ z = z.reshape((1,))
199
+
200
+ zf = z.ravel()
201
+ out = numpy.empty((zf.size, 3), dtype=numpy.complex128)
202
+ for i in range(zf.size):
203
+ out[i, :] = self._roots_cubic_scalar(zf[i])
204
+
205
+ out = out.reshape(z.shape + (3,))
206
+ if scalar:
207
+ return out.reshape((3,))
208
+ return out
209
+
210
+ # =======
211
+ # support
212
+ # =======
213
+
214
+ def support(self, y_probe=1e-6):
215
+ """
216
+ """
217
+
218
+ # Unpack parameters
219
+ t1 = self.t1
220
+ t2 = self.t2
221
+ w1 = self.w1
222
+ sigma = self.sigma
223
+
224
+ w2 = 1.0 - w1
225
+
226
+ p_a = numpy.poly1d([-1.0, t1])
227
+ p_b = numpy.poly1d([-1.0, t2])
228
+
229
+ pa2 = p_a * p_a
230
+ pb2 = p_b * p_b
231
+
232
+ eq = pa2 * pb2 - (sigma * sigma) * (w1 * pb2 + w2 * pa2)
233
+ u_roots = numpy.roots(eq.coeffs)
234
+
235
+ ucrit = []
236
+ for r in u_roots:
237
+ if numpy.isfinite(r) and abs(r.imag) < 1e-10:
238
+ ucrit.append(float(r.real))
239
+ ucrit.sort()
240
+
241
+ def G(u):
242
+ return w1 / (t1 - u) + w2 / (t2 - u)
243
+
244
+ def z_of_u(u):
245
+ return u - (sigma * sigma) * G(u)
246
+
247
+ edges = []
248
+ for u in ucrit:
249
+ x = z_of_u(u)
250
+ if numpy.isfinite(x):
251
+ x = float(numpy.real(x))
252
+ if (len(edges) == 0) or (abs(x - edges[-1]) > 1e-8):
253
+ edges.append(x)
254
+
255
+ if len(edges) < 2:
256
+ return []
257
+
258
+ thr = 100.0 * float(y_probe)
259
+ cuts = []
260
+ for i in range(len(edges) - 1):
261
+ xm = 0.5 * (edges[i] + edges[i + 1])
262
+ z = xm + 1j * float(y_probe)
263
+ r = self._roots_cubic_scalar(z)
264
+ m = _pick_physical_root_scalar(z, r)
265
+ if numpy.imag(m) > thr:
266
+ cuts.append((edges[i], edges[i + 1]))
267
+
268
+ return cuts
269
+
270
+ # ======
271
+ # matrix
272
+ # ======
273
+
274
+ def matrix(self, size, seed=None):
275
+ """
276
+ Generate matrix with the spectral density of the distribution.
277
+
278
+ Parameters
279
+ ----------
280
+
281
+ size : int
282
+ Size :math:`n` of the matrix.
283
+
284
+ seed : int, default=None
285
+ Seed for random number generator.
286
+
287
+ Returns
288
+ -------
289
+
290
+ A : numpy.ndarray
291
+ A matrix of the size :math:`n \\times n`.
292
+
293
+ Notes
294
+ -----
295
+
296
+ Generate an :math:`n \\times n` matrix
297
+ :math:`\\mathbf{A} = \\mathbf{T} + \\sigma \\mathbf{W}`
298
+ whose ESD converges to
299
+ :math:`H \\boxplus \\mathrm{SC}_{\\sigma^2}`, where
300
+ :math:`H = w_1 \\delta_{t_1} + (1 - w_1) \\delta_{t_2}`.
301
+
302
+ Examples
303
+ --------
304
+
305
+ .. code-block::python
306
+
307
+ >>> from freealg.distributions import DeformedWigner
308
+ >>> dwg = DeformedWigner(1/50)
309
+ >>> A = dwg.matrix(2000)
310
+ """
311
+
312
+ n = int(size)
313
+ if n <= 0:
314
+ raise ValueError("size must be a positive integer.")
315
+
316
+ # Unpack parameters
317
+ t1 = float(self.t1)
318
+ t2 = float(self.t2)
319
+ w1 = float(self.w1)
320
+ sigma = float(self.sigma)
321
+
322
+ # RNG
323
+ rng = numpy.random.default_rng(seed)
324
+
325
+ # T part
326
+ n1 = int(round(w1 * n))
327
+ n1 = max(0, min(n, n1))
328
+
329
+ d = numpy.empty(n, dtype=numpy.float64)
330
+ d[:n1] = t1
331
+ d[n1:] = t2
332
+ rng.shuffle(d) # randomize positions
333
+ T = numpy.diag(d)
334
+
335
+ # W part: Symmetric Wigner with variance 1/n (up to symmetry)
336
+ G = rng.standard_normal((n, n))
337
+ W = (G + G.T) * (0.5 / numpy.sqrt(n))
338
+
339
+ # Compose
340
+ A = T + sigma * W
341
+
342
+ return A
343
+
344
+ # ====
345
+ # poly
346
+ # ====
347
+
348
+ def poly(self):
349
+ """
350
+ Return a_coeffs for the exact cubic P(z,m)=0 of the two-atom deformed
351
+ Wigner model.
352
+
353
+ a_coeffs[i, j] is the coefficient of z^i m^j.
354
+ Shape is (deg_z+1, deg_m+1) = (3, 4).
355
+ """
356
+
357
+ t1 = float(self.t1)
358
+ t2 = float(self.t2)
359
+ w1 = float(self.w1)
360
+ w2 = 1.0 - w1
361
+ sigma = float(self.sigma)
362
+ s2 = sigma * sigma
363
+
364
+ a = numpy.zeros((3, 4), dtype=numpy.complex128)
365
+
366
+ # m^0 column (a0(z) = z - (w1 t2 + w2 t1))
367
+ a[0, 0] = -(w1 * t2 + w2 * t1)
368
+ a[1, 0] = 1.0
369
+ a[2, 0] = 0.0
370
+
371
+ # m^1 column (a1(z) = z^2 - (t1+t2)z + t1 t2 + s2)
372
+ a[0, 1] = t1 * t2 + s2
373
+ a[1, 1] = -(t1 + t2)
374
+ a[2, 1] = 1.0
375
+
376
+ # m^2 column (a2(z) = s2 z - s2 (t1+t2))
377
+ a[0, 2] = -s2 * (t1 + t2)
378
+ a[1, 2] = 2.0 * s2
379
+ a[2, 2] = 0.0
380
+
381
+ # m^3 column (a3(z) = s2^2)
382
+ a[0, 3] = s2 * s2
383
+ a[1, 3] = 0.0
384
+ a[2, 3] = 0.0
385
+
386
+ return a
@@ -13,8 +13,8 @@
13
13
 
14
14
  import numpy
15
15
  from scipy.interpolate import interp1d
16
- from .._plot_util import plot_density, plot_hilbert, plot_stieltjes, \
17
- plot_stieltjes_on_disk, plot_samples
16
+ from .._free_form._plot_util import plot_density, plot_hilbert, \
17
+ plot_stieltjes, plot_stieltjes_on_disk, plot_samples
18
18
 
19
19
  try:
20
20
  from scipy.integrate import cumtrapz
@@ -78,7 +78,7 @@ class KestenMcKay(object):
78
78
  ----------
79
79
 
80
80
  .. [1] Kesten, H. (1959). Symmetric random walks on groups. Transactions of
81
- the American Mathematical Society, 92(2), 336354.
81
+ the American Mathematical Society, 92(2), 336-354.
82
82
 
83
83
  .. [2] McKay, B. D. (1981). The expected eigenvalue distribution of a large
84
84
  regular graph. Linear Algebra and its Applications, 40, 203-216
@@ -110,7 +110,7 @@ class KestenMcKay(object):
110
110
  # density
111
111
  # =======
112
112
 
113
- def density(self, x=None, plot=False, latex=False, save=False):
113
+ def density(self, x=None, plot=False, latex=False, save=False, eig=None):
114
114
  """
115
115
  Density of distribution.
116
116
 
@@ -137,6 +137,10 @@ class KestenMcKay(object):
137
137
  assumed to the save filename (with the file extension). This option
138
138
  is relevant only if ``plot=True``.
139
139
 
140
+ eig : numpy.array, default=None
141
+ A collection of eigenvalues to compare to via histogram. This
142
+ option is relevant only if ``plot=True``.
143
+
140
144
  Returns
141
145
  -------
142
146
 
@@ -173,7 +177,11 @@ class KestenMcKay(object):
173
177
  numpy.sqrt(4.0 * (self.d - 1.0) - x[mask]**2)
174
178
 
175
179
  if plot:
176
- plot_density(x, rho, label='', latex=latex, save=save)
180
+ if eig is not None:
181
+ label = 'Theoretical'
182
+ else:
183
+ label = ''
184
+ plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
177
185
 
178
186
  return rho
179
187
 
@@ -494,9 +502,6 @@ class KestenMcKay(object):
494
502
  :class: custom-dark
495
503
  """
496
504
 
497
- if seed is not None:
498
- numpy.random.seed(seed)
499
-
500
505
  if x_min is None:
501
506
  x_min = self.lam_m
502
507
 
@@ -515,14 +520,23 @@ class KestenMcKay(object):
515
520
  inv_cdf = interp1d(cdf, xs, bounds_error=False,
516
521
  fill_value=(x_min, x_max))
517
522
 
523
+ # Random generator
524
+ rng = numpy.random.default_rng(seed)
525
+
518
526
  # Draw from uniform distribution
519
527
  if method == 'mc':
520
- u = numpy.random.rand(size)
528
+ u = rng.random(size)
529
+
521
530
  elif method == 'qmc':
522
- engine = qmc.Halton(d=1)
523
- u = engine.random(size)
531
+ try:
532
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
533
+ except TypeError:
534
+ # Older scipy versions
535
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
536
+ u = engine.random(size).ravel()
537
+
524
538
  else:
525
- raise ValueError('"method" is invalid.')
539
+ raise NotImplementedError('"method" is invalid.')
526
540
 
527
541
  # Draw from distribution by mapping from inverse CDF
528
542
  samples = inv_cdf(u).ravel()
@@ -539,9 +553,9 @@ class KestenMcKay(object):
539
553
 
540
554
  return samples
541
555
 
542
- # ============
543
- # Haar unitary
544
- # ============
556
+ # ===============
557
+ # haar orthogonal
558
+ # ===============
545
559
 
546
560
  def _haar_orthogonal(self, n, k, seed=None):
547
561
  """