freealg 0.0.3__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
freealg/freeform.py CHANGED
@@ -22,6 +22,7 @@ from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
22
22
  from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
23
23
  from ._pade import fit_pade, eval_pade
24
24
  from ._decompress import decompress
25
+ from ._sample import qmc_sample
25
26
 
26
27
  __all__ = ['FreeForm']
27
28
 
@@ -173,8 +174,8 @@ class FreeForm(object):
173
174
  # ===
174
175
 
175
176
  def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, reg=0.0,
176
- damp=None, force=False, pade_p=1, pade_q=1, plot=False,
177
- latex=False, save=False):
177
+ damp=None, force=False, pade_p=0, pade_q=1, odd_side='left',
178
+ optimizer='ls', plot=False, latex=False, save=False):
178
179
  """
179
180
  Fit model to eigenvalues.
180
181
 
@@ -209,15 +210,30 @@ class FreeForm(object):
209
210
  If `True`, it forces the density to have unit mass and to be
210
211
  strictly positive.
211
212
 
212
- pade_p : int, default=1
213
- Degree of polynomial :math:`P(z)`. See notes below.
213
+ pade_p : int, default=0
214
+ Degree of polynomial :math:`P(z)` is :math:`q+p` where :math:`p`
215
+ can only be ``-1``, ``0``, or ``1``. See notes below.
214
216
 
215
217
  pade_q : int, default=1
216
- Degree of polynomial :math:`Q(z)`. See notes below.
218
+ Degree of polynomial :math:`Q(z)` is :math:`q`. See notes below.
219
+
220
+ odd_side : {``'left'``, ``'right'``}, default= ``'left'``
221
+ In case of odd number of poles (when :math:`q` is odd), the extra
222
+ pole is set to the left or right side of the support interval,
223
+ while all other poles are split in half to the left and right. Note
224
+ that this is only for the initialization of the poles. The
225
+ optimizer will decide best location by moving them to the left or
226
+ right of the support.
227
+
228
+ optimizer : {``'ls'``, ``'de'``}, default= ``'ls'``
229
+ Optimizer for Pade approximation, including:
230
+
231
+ * ``'ls'``: least square (local, fast)
232
+ * ``'de'``: differential evolution (global, slow)
217
233
 
218
234
  plot : bool, default=False
219
- If `True`, the approximation coefficients and pade approximaton to
220
- the Hilbert traosnform are plotted.
235
+ If `True`, the approximation coefficients and Pade approximation to
236
+ the Hilbert transform are plotted.
221
237
 
222
238
  latex : bool, default=False
223
239
  If `True`, the plot is rendered using LaTeX. This option is
@@ -234,6 +250,20 @@ class FreeForm(object):
234
250
  psi : (K+1, ) numpy.ndarray
235
251
  Coefficients of fitting Jacobi polynomials
236
252
 
253
+ Notes
254
+ -----
255
+
256
+ The Pade approximation for the glue function :math:`G(z)` is
257
+
258
+ .. math::
259
+
260
+ G(z) = \\frac{P(z)}{Q(z)},
261
+
262
+ where :math:`P(z)` and :math:`Q(z)` are polynomials of order
263
+ :math:`p+q` and :math:`q` respectively. Note that :math:`p` can only
264
+ be -1, 0, or 1, effectively making Pade approximation of order
265
+ :math:`q-1:q`, :math:`q:q`, or :math:`q-1:q`.
266
+
237
267
  Examples
238
268
  --------
239
269
 
@@ -299,17 +329,16 @@ class FreeForm(object):
299
329
  g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
300
330
 
301
331
  # Fit a pade approximation
302
- self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m,
303
- self.lam_p, pade_p, pade_q, delta=1e-8,
304
- B=numpy.inf, S=numpy.inf)
332
+ # self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m,
333
+ # self.lam_p, pade_p, pade_q, delta=1e-8,
334
+ # B=numpy.inf, S=numpy.inf)
335
+ self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
336
+ p=pade_p, q=pade_q, odd_side=odd_side,
337
+ safety=1.0, max_outer=40, xtol=1e-12,
338
+ ftol=1e-12, optimizer=optimizer, verbose=0)
305
339
 
306
340
  if plot:
307
- # Unpack optimized parameters
308
- s = self._pade_sol['s']
309
- a = self._pade_sol['a']
310
- b = self._pade_sol['b']
311
-
312
- g_supp_approx = eval_pade(x_supp[None, :], s, a, b)[0, :]
341
+ g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
313
342
  plot_fit(psi, x_supp, g_supp, g_supp_approx, support=self.support,
314
343
  latex=latex, save=save)
315
344
 
@@ -513,13 +542,8 @@ class FreeForm(object):
513
542
  """
514
543
  """
515
544
 
516
- # Unpack optimized parameters
517
- s = self._pade_sol['s']
518
- a = self._pade_sol['a']
519
- b = self._pade_sol['b']
520
-
521
545
  # Glue function
522
- g = eval_pade(z, s, a, b)
546
+ g = eval_pade(z, self._pade_sol)
523
547
 
524
548
  return g
525
549
 
@@ -633,7 +657,7 @@ class FreeForm(object):
633
657
  m1[mask_m, :] = numpy.conjugate(
634
658
  stieltjes(numpy.conjugate(z[mask_m, :])))
635
659
 
636
- # Second Reimann sheet
660
+ # Second Riemann sheet
637
661
  m2[mask_p, :] = m1[mask_p, :]
638
662
  m2[mask_m, :] = -m1[mask_m, :] + self._glue(z[mask_m, :])
639
663
 
@@ -721,7 +745,7 @@ class FreeForm(object):
721
745
  m1[mask_m] = numpy.conjugate(
722
746
  stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).reshape(-1)
723
747
 
724
- # Second Reimann sheet
748
+ # Second Riemann sheet
725
749
  m2[mask_p] = m1[mask_p]
726
750
  m2[mask_m] = -m1[mask_m] + self._glue(
727
751
  z[mask_m].reshape(-1, 1)).reshape(-1)
@@ -734,7 +758,7 @@ class FreeForm(object):
734
758
  # decompress
735
759
  # ==========
736
760
 
737
- def decompress(self, size, x=None, delta=1e-4, iterations=500,
761
+ def decompress(self, size, x=None, delta=1e-6, iterations=500,
738
762
  step_size=0.1, tolerance=1e-4, plot=False, latex=False,
739
763
  save=False):
740
764
  """
@@ -782,6 +806,10 @@ class FreeForm(object):
782
806
  rho : numpy.array
783
807
  Spectral density
784
808
 
809
+ eigs : numpy.array
810
+ Estimated eigenvalues as low-discrepancy samples of the estimated
811
+ spectral density.
812
+
785
813
  See Also
786
814
  --------
787
815
 
@@ -809,9 +837,12 @@ class FreeForm(object):
809
837
  rho, x, (lb, ub) = decompress(self, size, x=x, delta=delta,
810
838
  iterations=iterations,
811
839
  step_size=step_size, tolerance=tolerance)
840
+ x, rho = x.ravel(), rho.ravel()
812
841
 
813
842
  if plot:
814
- plot_density(x.reshape(-1), rho.reshape(-1), support=(lb, ub),
843
+ plot_density(x, rho, support=(lb, ub),
815
844
  label='Decompression', latex=latex, save=save)
816
845
 
817
- return rho
846
+ eigs = numpy.sort(qmc_sample(x, rho, size))
847
+
848
+ return rho, eigs