freealg 0.0.3__py3-none-any.whl → 0.1.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.
@@ -0,0 +1,559 @@
1
+ # SPDX-FileCopyrightText: Copyright 2025, 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
+ import networkx as nx
16
+ from scipy.interpolate import interp1d
17
+ from .._plot_util import plot_density, plot_hilbert, plot_stieltjes, \
18
+ plot_stieltjes_on_disk, plot_samples
19
+
20
+ try:
21
+ from scipy.integrate import cumtrapz
22
+ except ImportError:
23
+ from scipy.integrate import cumulative_trapezoid as cumtrapz
24
+
25
+ __all__ = ['KestenMcKay']
26
+
27
+
28
+ # ============
29
+ # Kesten McKay
30
+ # ============
31
+
32
+ class KestenMcKay(object):
33
+ """
34
+ Kesten-McKay distribution.
35
+
36
+ Parameters
37
+ ----------
38
+
39
+ d : float
40
+ Parameter :math:`d` of the distribution. See Notes.
41
+
42
+ Methods
43
+ -------
44
+
45
+ density
46
+ Spectral density of distribution.
47
+
48
+ hilbert
49
+ Hilbert transform of distribution.
50
+
51
+ stieltjes
52
+ Stieltjes transform of distribution.
53
+
54
+ sample
55
+ Sample from distribution.
56
+
57
+ matrix
58
+ Generate matrix with its empirical spectral density of distribution
59
+
60
+ Notes
61
+ -----
62
+
63
+ The Marchenko-Pastur distribution has the absolutely-continuous density
64
+
65
+ .. math::
66
+
67
+ \\mathrm{d} \\rho(x) =
68
+ \\frac{\\sqrt{4(d-1) - x^2}}{2 \\pi (d^2 - x^2)}
69
+ \\mathbf{1}_{x \\in [\\lambda_{-}, \\lambda_{+}]} \\mathrm{d}{x}
70
+
71
+ where
72
+
73
+ * :math:`\\lambda_{\\pm} = \\pm 2 \\sqrt{d-1}` are the edges of
74
+ the support.
75
+ * :math:`d > 1` is the shape parameter of the density.
76
+
77
+ References
78
+ ----------
79
+
80
+ .. [1] Kesten, H. (1959). Symmetric random walks on groups. Transactions of
81
+ the American Mathematical Society, 92(2), 336–354.
82
+
83
+ .. [2] McKay, B. D. (1981). The expected eigenvalue distribution of a large
84
+ regular graph. Linear Algebra and its Applications, 40, 203-216
85
+
86
+ Examples
87
+ --------
88
+
89
+ .. code-block:: python
90
+
91
+ >>> from freealg.distributions import KestenMcKay
92
+ >>> km = KestenMcKay()
93
+ """
94
+
95
+ # ====
96
+ # init
97
+ # ====
98
+
99
+ def __init__(self, d):
100
+ """
101
+ Initialization.
102
+ """
103
+
104
+ self.d = d
105
+ self.lam_p = 2.0 * numpy.sqrt(d - 1.0)
106
+ self.lam_m = -2.0 * numpy.sqrt(d - 1.0)
107
+ self.support = (self.lam_m, self.lam_p)
108
+
109
+ # =======
110
+ # density
111
+ # =======
112
+
113
+ def density(self, x=None, plot=False, latex=False, save=False):
114
+ """
115
+ Density of distribution.
116
+
117
+ Parameters
118
+ ----------
119
+
120
+ x : numpy.array, default=None
121
+ The locations where density is evaluated at. If `None`, an interval
122
+ slightly larger than the support interval of the spectral density
123
+ is used.
124
+
125
+ rho : numpy.array, default=None
126
+ Density. If `None`, it will be computed.
127
+
128
+ plot : bool, default=False
129
+ If `True`, density is plotted.
130
+
131
+ latex : bool, default=False
132
+ If `True`, the plot is rendered using LaTeX. This option is
133
+ relevant only if ``plot=True``.
134
+
135
+ save : bool, default=False
136
+ If not `False`, the plot is saved. If a string is given, it is
137
+ assumed to the save filename (with the file extension). This option
138
+ is relevant only if ``plot=True``.
139
+
140
+ Returns
141
+ -------
142
+
143
+ rho : numpy.array
144
+ Density.
145
+
146
+ Examples
147
+ --------
148
+
149
+ .. code-block::python
150
+
151
+ >>> from freealg.distributions import KestenMcKay
152
+ >>> km = KestenMcKay(3)
153
+ >>> rho = km.density(plot=True)
154
+
155
+ .. image:: ../_static/images/plots/km_density.png
156
+ :align: center
157
+ :class: custom-dark
158
+ """
159
+
160
+ # Create x if not given
161
+ if x is None:
162
+ radius = 0.5 * (self.lam_p - self.lam_m)
163
+ center = 0.5 * (self.lam_p + self.lam_m)
164
+ scale = 1.25
165
+ x_min = numpy.floor(center - radius * scale)
166
+ x_max = numpy.ceil(center + radius * scale)
167
+ x = numpy.linspace(x_min, x_max, 500)
168
+
169
+ rho = numpy.zeros_like(x)
170
+ mask = numpy.logical_and(x > self.lam_m, x < self.lam_p)
171
+
172
+ rho[mask] = (self.d / (2.0 * numpy.pi * (self.d**2 - x[mask]**2))) * \
173
+ numpy.sqrt(4.0 * (self.d - 1.0) - x[mask]**2)
174
+
175
+ if plot:
176
+ plot_density(x, rho, label='', latex=latex, save=save)
177
+
178
+ return rho
179
+
180
+ # =======
181
+ # hilbert
182
+ # =======
183
+
184
+ def hilbert(self, x=None, plot=False, latex=False, save=False):
185
+ """
186
+ Hilbert transform of the distribution.
187
+
188
+ Parameters
189
+ ----------
190
+
191
+ x : numpy.array, default=None
192
+ The locations where Hilbert transform is evaluated at. If `None`,
193
+ an interval slightly larger than the support interval of the
194
+ spectral density is used.
195
+
196
+ plot : bool, default=False
197
+ If `True`, Hilbert transform is plotted.
198
+
199
+ latex : bool, default=False
200
+ If `True`, the plot is rendered using LaTeX. This option is
201
+ relevant only if ``plot=True``.
202
+
203
+ save : bool, default=False
204
+ If not `False`, the plot is saved. If a string is given, it is
205
+ assumed to the save filename (with the file extension). This option
206
+ is relevant only if ``plot=True``.
207
+
208
+ Returns
209
+ -------
210
+
211
+ hilb : numpy.array
212
+ Hilbert transform.
213
+
214
+ Examples
215
+ --------
216
+
217
+ .. code-block::python
218
+
219
+ >>> from freealg.distributions import KestenMcKay
220
+ >>> km = KestenMcKay(3)
221
+ >>> hilb = km.hilbert(plot=True)
222
+
223
+ .. image:: ../_static/images/plots/km_hilbert.png
224
+ :align: center
225
+ :class: custom-dark
226
+ """
227
+
228
+ # Create x if not given
229
+ if x is None:
230
+ radius = 0.5 * (self.lam_p - self.lam_m)
231
+ center = 0.5 * (self.lam_p + self.lam_m)
232
+ scale = 1.25
233
+ x_min = numpy.floor(center - radius * scale)
234
+ x_max = numpy.ceil(center + radius * scale)
235
+ x = numpy.linspace(x_min, x_max, 500)
236
+
237
+ def _P(x):
238
+ return (self.d - 2.0) * x
239
+
240
+ def _Q(x):
241
+ return self.d**2 - x**2
242
+
243
+ P = _P(x)
244
+ Q = _Q(x)
245
+ Delta2 = P**2 - 4.0 * Q
246
+ Delta = numpy.sqrt(numpy.maximum(Delta2, 0))
247
+ sign = numpy.sign(P)
248
+ hilb = (P - sign * Delta) / (2.0 * Q)
249
+
250
+ # using negative sign convention
251
+ hilb = -hilb
252
+
253
+ if plot:
254
+ plot_hilbert(x, hilb, support=self.support, latex=latex, save=save)
255
+
256
+ return hilb
257
+
258
+ # =======================
259
+ # m mp numeric vectorized
260
+ # =======================
261
+
262
+ def _m_mp_numeric_vectorized(self, z, alt_branch=False, tol=1e-8):
263
+ """
264
+ Stieltjes transform (principal or secondary branch)
265
+ for Marchenko–Pastur distribution on upper half-plane.
266
+ """
267
+
268
+ m = numpy.empty_like(z, dtype=complex)
269
+
270
+ sign = -1 if alt_branch else 1
271
+ A = self.d**2 - z**2
272
+ B = (self.d - 2.0) * z
273
+ D = B**2 - 4 * A
274
+ sqrtD = numpy.sqrt(D)
275
+ m1 = (-B + sqrtD) / (2 * A)
276
+ m2 = (-B - sqrtD) / (2 * A)
277
+
278
+ # pick correct branch
279
+ upper = z.imag >= 0
280
+ branch = numpy.empty_like(m1)
281
+ branch[upper] = numpy.where(sign*m1[upper].imag > 0, m1[upper],
282
+ m2[upper])
283
+ branch[~upper] = numpy.where(sign*m1[~upper].imag < 0, m1[~upper],
284
+ m2[~upper])
285
+ m = branch
286
+
287
+ return m
288
+
289
+ # ============
290
+ # m mp reflect
291
+ # ============
292
+
293
+ def _m_mp_reflect(self, z, alt_branch=False):
294
+ """
295
+ Analytic continuation using Schwarz reflection.
296
+ """
297
+
298
+ mask_p = z.imag >= 0.0
299
+ mask_n = z.imag < 0.0
300
+
301
+ m = numpy.zeros_like(z)
302
+
303
+ f = self._m_mp_numeric_vectorized
304
+ m[mask_p] = f(z[mask_p], alt_branch=False)
305
+ m[mask_n] = f(z[mask_n], alt_branch=alt_branch)
306
+
307
+ return m
308
+
309
+ # =========
310
+ # stieltjes
311
+ # =========
312
+
313
+ def stieltjes(self, x=None, y=None, plot=False, on_disk=False, latex=False,
314
+ save=False):
315
+ """
316
+ Stieltjes transform of distribution.
317
+
318
+ Parameters
319
+ ----------
320
+
321
+ x : numpy.array, default=None
322
+ The x axis of the grid where the Stieltjes transform is evaluated.
323
+ If `None`, an interval slightly larger than the support interval of
324
+ the spectral density is used.
325
+
326
+ y : numpy.array, default=None
327
+ The y axis of the grid where the Stieltjes transform is evaluated.
328
+ If `None`, a grid on the interval ``[-1, 1]`` is used.
329
+
330
+ plot : bool, default=False
331
+ If `True`, Stieltjes transform is plotted.
332
+
333
+ on_disk : bool, default=False
334
+ If `True`, the Stieltjes transform is mapped on unit disk. This
335
+ option relevant only if ``plot=True``.
336
+
337
+ latex : bool, default=False
338
+ If `True`, the plot is rendered using LaTeX. This option is
339
+ relevant only if ``plot=True``.
340
+
341
+ save : bool, default=False
342
+ If not `False`, the plot is saved. If a string is given, it is
343
+ assumed to the save filename (with the file extension). This option
344
+ is relevant only if ``plot=True``.
345
+
346
+ Returns
347
+ -------
348
+
349
+ m1 : numpy.array
350
+ Stieltjes transform on principal branch.
351
+
352
+ m12 : numpy.array
353
+ Stieltjes transform on secondary branch.
354
+
355
+ Examples
356
+ --------
357
+
358
+ .. code-block:: python
359
+
360
+ >>> from freealg.distributions import KestenMcKay
361
+ >>> km = KestenMcKay(3)
362
+ >>> m1, m2 = km.stieltjes(plot=True)
363
+
364
+ .. image:: ../_static/images/plots/km_stieltjes.png
365
+ :align: center
366
+ :class: custom-dark
367
+
368
+ Plot on unit disk using Cayley transform:
369
+
370
+ .. code-block:: python
371
+
372
+ >>> m1, m2 = mp.stieltjes(plot=True, on_disk=True)
373
+
374
+ .. image:: ../_static/images/plots/km_stieltjes_disk.png
375
+ :align: center
376
+ :class: custom-dark
377
+ """
378
+
379
+ if (plot is True) and (on_disk is True):
380
+ n_r = 1000
381
+ n_t = 1000
382
+ r_min, r_max = 0, 2.5
383
+ t_min, t_max = 0, 2.0 * numpy.pi
384
+ r = numpy.linspace(r_min, r_max, n_r)
385
+ t = numpy.linspace(t_min, t_max, n_t + 1)[:-1]
386
+ grid_r, grid_t = numpy.meshgrid(r, t)
387
+
388
+ grid_x_D = grid_r * numpy.cos(grid_t)
389
+ grid_y_D = grid_r * numpy.sin(grid_t)
390
+ zeta = grid_x_D + 1j * grid_y_D
391
+
392
+ # Cayley transform mapping zeta on D to z on H
393
+ z_H = 1j * (1 + zeta) / (1 - zeta)
394
+
395
+ m1_D = self._m_mp_reflect(z_H, alt_branch=False)
396
+ m2_D = self._m_mp_reflect(z_H, alt_branch=True)
397
+
398
+ plot_stieltjes_on_disk(r, t, m1_D, m2_D, support=self.support,
399
+ latex=latex, save=save)
400
+
401
+ return m1_D, m2_D
402
+
403
+ # Create x if not given
404
+ if x is None:
405
+ radius = 0.5 * (self.lam_p - self.lam_m)
406
+ center = 0.5 * (self.lam_p + self.lam_m)
407
+ scale = 2.0
408
+ x_min = numpy.floor(2.0 * (center - 2.0 * radius * scale)) / 2.0
409
+ x_max = numpy.ceil(2.0 * (center + 2.0 * radius * scale)) / 2.0
410
+ x = numpy.linspace(x_min, x_max, 500)
411
+
412
+ # Create y if not given
413
+ if y is None:
414
+ y = numpy.linspace(-1, 1, 400)
415
+
416
+ x_grid, y_grid = numpy.meshgrid(x, y)
417
+ z = x_grid + 1j * y_grid # shape (Ny, Nx)
418
+
419
+ m1 = self._m_mp_reflect(z, alt_branch=False)
420
+ m2 = self._m_mp_reflect(z, alt_branch=True)
421
+
422
+ if plot:
423
+ plot_stieltjes(x, y, m1, m2, support=self.support, latex=latex,
424
+ save=save)
425
+
426
+ return m1, m2
427
+
428
+ # ======
429
+ # sample
430
+ # ======
431
+
432
+ def sample(self, size, x_min=None, x_max=None, plot=False, latex=False,
433
+ save=False):
434
+ """
435
+ Sample from distribution.
436
+
437
+ Parameters
438
+ ----------
439
+
440
+ size : int
441
+ Size of sample.
442
+
443
+ x_min : float, default=None
444
+ Minimum of sample values. If `None`, the left edge of the support
445
+ is used.
446
+
447
+ x_max : float, default=None
448
+ Maximum of sample values. If `None`, the right edge of the support
449
+ is used.
450
+
451
+ plot : bool, default=False
452
+ If `True`, samples histogram is plotted.
453
+
454
+ latex : bool, default=False
455
+ If `True`, the plot is rendered using LaTeX. This option is
456
+ relevant only if ``plot=True``.
457
+
458
+ save : bool, default=False
459
+ If not `False`, the plot is saved. If a string is given, it is
460
+ assumed to the save filename (with the file extension). This option
461
+ is relevant only if ``plot=True``.
462
+
463
+ Returns
464
+ -------
465
+
466
+ s : numpy.ndarray
467
+ Samples.
468
+
469
+ Notes
470
+ -----
471
+
472
+ This method uses inverse transform sampling.
473
+
474
+ Examples
475
+ --------
476
+
477
+ .. code-block::python
478
+
479
+ >>> from freealg.distributions import KestenMcKay
480
+ >>> km = KestenMcKay(3)
481
+ >>> s = km.sample(2000)
482
+
483
+ .. image:: ../_static/images/plots/km_samples.png
484
+ :align: center
485
+ :class: custom-dark
486
+ """
487
+
488
+ if x_min is None:
489
+ x_min = self.lam_m
490
+
491
+ if x_max is None:
492
+ x_max = self.lam_p
493
+
494
+ # Grid and PDF
495
+ xs = numpy.linspace(x_min, x_max, size)
496
+ pdf = self.density(xs)
497
+
498
+ # CDF (using cumulative trapezoidal rule)
499
+ cdf = cumtrapz(pdf, xs, initial=0)
500
+ cdf /= cdf[-1] # normalize CDF to 1
501
+
502
+ # Inverse CDF interpolator
503
+ inv_cdf = interp1d(cdf, xs, bounds_error=False,
504
+ fill_value=(x_min, x_max))
505
+
506
+ # Sample and map
507
+ u = numpy.random.rand(size)
508
+ samples = inv_cdf(u)
509
+
510
+ if plot:
511
+ radius = 0.5 * (self.lam_p - self.lam_m)
512
+ center = 0.5 * (self.lam_p + self.lam_m)
513
+ scale = 1.25
514
+ x_min = numpy.floor(center - radius * scale)
515
+ x_max = numpy.ceil(center + radius * scale)
516
+ x = numpy.linspace(x_min, x_max, 500)
517
+ rho = self.density(x)
518
+ plot_samples(x, rho, x_min, x_max, samples, latex=latex, save=save)
519
+
520
+ return samples
521
+
522
+ # ======
523
+ # matrix
524
+ # ======
525
+
526
+ def matrix(self, size):
527
+ """
528
+ Generate matrix with the spectral density of the distribution.
529
+
530
+ Parameters
531
+ ----------
532
+
533
+ size : int
534
+ Size :math:`n` of the matrix.
535
+
536
+ Returns
537
+ -------
538
+
539
+ A : numpy.ndarray
540
+ A matrix of the size :math:`n \\times n`.
541
+
542
+ Examples
543
+ --------
544
+
545
+ .. code-block::python
546
+
547
+ >>> from freealg.distributions import KestenMcKay
548
+ >>> km = KestenMcKay(3)
549
+ >>> A = km.matrix(2000)
550
+ """
551
+
552
+ n = size
553
+ G = nx.random_regular_graph(self.d, n)
554
+ A = nx.to_numpy_array(G, dtype=float) # shape (n,n)
555
+
556
+ mu = self.d / n
557
+ A_c = A - mu * numpy.ones((n, n))
558
+
559
+ return A_c
@@ -69,8 +69,9 @@ class MarchenkoPastur(object):
69
69
 
70
70
  where
71
71
 
72
- * :math:`\\lambda_{\\pm} = (1 \\pm \\sqrt{\\lambda})^2`
73
- * :math:`\\lambda > 0` the parameter of the shape density.
72
+ * :math:`\\lambda_{\\pm} = (1 \\pm \\sqrt{\\lambda})^2` are the edges of
73
+ the support.
74
+ * :math:`\\lambda > 0` is the shape parameter of the density.
74
75
 
75
76
  References
76
77
  ----------
@@ -483,8 +484,8 @@ class MarchenkoPastur(object):
483
484
 
484
485
  >>> from freealg.distributions import MarchenkoPastur
485
486
  >>> mp = MarchenkoPastur(1/50)
487
+ >>> s = mp.sample(2000)
486
488
 
487
- >>> s = mp.sample(2000)
488
489
  .. image:: ../_static/images/plots/mp_samples.png
489
490
  :align: center
490
491
  :class: custom-dark