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