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
@@ -13,7 +13,7 @@
13
13
 
14
14
  import numpy
15
15
  from functools import partial
16
- from .._util import resolve_complex_dtype, compute_eig
16
+ from .._util import compute_eig
17
17
  from ._density_util import kde, force_density
18
18
  from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_density, \
19
19
  jacobi_stieltjes
@@ -21,11 +21,13 @@ from ._chebyshev import chebyshev_sample_proj, chebyshev_kernel_proj, \
21
21
  chebyshev_density, chebyshev_stieltjes
22
22
  from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
23
23
  exponential_damping, parzen_damping
24
- from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
24
+ from ._plot_util import plot_fit
25
+ from ..visualization._plot_util import plot_density, plot_hilbert, \
26
+ plot_stieltjes
25
27
  from ._pade import fit_pade, eval_pade
26
28
  from ._decompress import decompress
27
- from ._sample import sample
28
- from ._support import supp
29
+ from .._support import supp
30
+ from .._base_form import BaseForm
29
31
 
30
32
  # Fallback to previous numpy API
31
33
  if not hasattr(numpy, 'trapezoid'):
@@ -38,7 +40,7 @@ __all__ = ['FreeForm']
38
40
  # Free Form
39
41
  # =========
40
42
 
41
- class FreeForm(object):
43
+ class FreeForm(BaseForm):
42
44
  """
43
45
  Free probability for large matrices.
44
46
 
@@ -143,12 +145,7 @@ class FreeForm(object):
143
145
  Initialization.
144
146
  """
145
147
 
146
- self.A = None
147
- self.eig = None
148
- self.delta = delta # Offset above real axis to apply Plemelj formula
149
-
150
- # Data type for complex arrays
151
- self.dtype = resolve_complex_dtype(dtype)
148
+ super().__init__(delta, dtype)
152
149
 
153
150
  # Eigenvalues
154
151
  if A.ndim == 1:
@@ -467,27 +464,6 @@ class FreeForm(object):
467
464
 
468
465
  return self.psi
469
466
 
470
- # =============
471
- # generate grid
472
- # =============
473
-
474
- def _generate_grid(self, scale, extend=1.0, N=500):
475
- """
476
- Generate a grid of points to evaluate density / Hilbert / Stieltjes
477
- transforms.
478
- """
479
-
480
- radius = 0.5 * (self.lam_p - self.lam_m)
481
- center = 0.5 * (self.lam_p + self.lam_m)
482
-
483
- x_min = numpy.floor(extend * (center - extend * radius * scale))
484
- x_max = numpy.ceil(extend * (center + extend * radius * scale))
485
-
486
- x_min /= extend
487
- x_max /= extend
488
-
489
- return numpy.linspace(x_min, x_max, N)
490
-
491
467
  # =======
492
468
  # density
493
469
  # =======
@@ -1025,338 +1001,3 @@ class FreeForm(object):
1025
1001
  label='Decompression', latex=latex, save=save)
1026
1002
 
1027
1003
  return rho, x
1028
-
1029
- # ========
1030
- # eigvalsh
1031
- # ========
1032
-
1033
- def eigvalsh(self, size=None, seed=None, **kwargs):
1034
- """
1035
- Estimate the eigenvalues.
1036
-
1037
- This function estimates the eigenvalues of the freeform matrix
1038
- or a larger matrix containing it using free decompression.
1039
-
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
- seed : int, default=None
1049
- The seed for the Quasi-Monte Carlo sampler.
1050
-
1051
- **kwargs : dict, optional
1052
- Pass additional options to the underlying
1053
- :func:`FreeForm.decompress` function.
1054
-
1055
- Returns
1056
- -------
1057
-
1058
- eigs : numpy.array
1059
- Eigenvalues of decompressed matrix
1060
-
1061
- See Also
1062
- --------
1063
-
1064
- FreeForm.decompress
1065
- FreeForm.cond
1066
-
1067
- Notes
1068
- -----
1069
-
1070
- All arguments to the `.decompress()` procedure can be provided.
1071
-
1072
- Examples
1073
- --------
1074
-
1075
- .. code-block:: python
1076
- :emphasize-lines: 1
1077
-
1078
- >>> from freealg import FreeForm
1079
- """
1080
-
1081
- if size is None:
1082
- size = self.n
1083
-
1084
- rho, x = self.decompress(size, **kwargs)
1085
- eigs = numpy.sort(sample(x, rho, size, method='qmc', seed=seed))
1086
-
1087
- return eigs
1088
-
1089
- # ====
1090
- # cond
1091
- # ====
1092
-
1093
- def cond(self, size=None, seed=None, **kwargs):
1094
- """
1095
- Estimate the condition number.
1096
-
1097
- This function estimates the condition number of the matrix
1098
- :math:`\\mathbf{A}` or a larger matrix containing :math:`\\mathbf{A}`
1099
- using free decompression.
1100
-
1101
- Parameters
1102
- ----------
1103
-
1104
- size : int, default=None
1105
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1106
- eigenvalues of. If None, returns estimates of the eigenvalues of
1107
- :math:`\\mathbf{A}` itself.
1108
-
1109
- **kwargs : dict, optional
1110
- Pass additional options to the underlying
1111
- :func:`FreeForm.decompress` function.
1112
-
1113
- Returns
1114
- -------
1115
-
1116
- c : float
1117
- Condition number
1118
-
1119
- See Also
1120
- --------
1121
-
1122
- FreeForm.eigvalsh
1123
- FreeForm.norm
1124
- FreeForm.slogdet
1125
- FreeForm.trace
1126
-
1127
- Examples
1128
- --------
1129
-
1130
- .. code-block:: python
1131
- :emphasize-lines: 1
1132
-
1133
- >>> from freealg import FreeForm
1134
- """
1135
-
1136
- eigs = self.eigvalsh(size=size, **kwargs)
1137
- return eigs.max() / eigs.min()
1138
-
1139
- # =====
1140
- # trace
1141
- # =====
1142
-
1143
- def trace(self, size=None, p=1.0, seed=None, **kwargs):
1144
- """
1145
- Estimate the trace of a power.
1146
-
1147
- This function estimates the trace of the matrix power
1148
- :math:`\\mathbf{A}^p` of the freeform or that of a larger matrix
1149
- containing it.
1150
-
1151
- Parameters
1152
- ----------
1153
-
1154
- size : int, default=None
1155
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1156
- eigenvalues of. If None, returns estimates of the eigenvalues of
1157
- :math:`\\mathbf{A}` itself.
1158
-
1159
- p : float, default=1.0
1160
- The exponent :math:`p` in :math:`\\mathbf{A}^p`.
1161
-
1162
- seed : int, default=None
1163
- The seed for the Quasi-Monte Carlo sampler.
1164
-
1165
- **kwargs : dict, optional
1166
- Pass additional options to the underlying
1167
- :func:`FreeForm.decompress` function.
1168
-
1169
- Returns
1170
- -------
1171
-
1172
- trace : float
1173
- matrix trace
1174
-
1175
- See Also
1176
- --------
1177
-
1178
- FreeForm.eigvalsh
1179
- FreeForm.cond
1180
- FreeForm.slogdet
1181
- FreeForm.norm
1182
-
1183
- Notes
1184
- -----
1185
-
1186
- The trace is highly amenable to subsampling: under free decompression
1187
- the average eigenvalue is assumed constant, so the trace increases
1188
- linearly. Traces of powers fall back to :func:`eigvalsh`.
1189
- All arguments to the `.decompress()` procedure can be provided.
1190
-
1191
- Examples
1192
- --------
1193
-
1194
- .. code-block:: python
1195
- :emphasize-lines: 1
1196
-
1197
- >>> from freealg import FreeForm
1198
- """
1199
-
1200
- if numpy.isclose(p, 1.0):
1201
- return numpy.mean(self.eig) * (size / self.n)
1202
-
1203
- eig = self.eigvalsh(size=size, seed=seed, **kwargs)
1204
- return numpy.sum(eig ** p)
1205
-
1206
- # =======
1207
- # slogdet
1208
- # =======
1209
-
1210
- def slogdet(self, size=None, seed=None, **kwargs):
1211
- """
1212
- Estimate the sign and logarithm of the determinant.
1213
-
1214
- This function estimates the *slogdet* of the freeform or that of
1215
- a larger matrix containing it using free decompression.
1216
-
1217
- Parameters
1218
- ----------
1219
-
1220
- size : int, default=None
1221
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1222
- eigenvalues of. If None, returns estimates of the eigenvalues of
1223
- :math:`\\mathbf{A}` itself.
1224
-
1225
- seed : int, default=None
1226
- The seed for the Quasi-Monte Carlo sampler.
1227
-
1228
- Returns
1229
- -------
1230
-
1231
- sign : float
1232
- Sign of determinant
1233
-
1234
- ld : float
1235
- natural logarithm of the absolute value of the determinant
1236
-
1237
- See Also
1238
- --------
1239
-
1240
- FreeForm.eigvalsh
1241
- FreeForm.cond
1242
- FreeForm.trace
1243
- FreeForm.norm
1244
-
1245
- Notes
1246
- -----
1247
-
1248
- All arguments to the `.decompress()` procedure can be provided.
1249
-
1250
- Examples
1251
- --------
1252
-
1253
- .. code-block:: python
1254
- :emphasize-lines: 1
1255
-
1256
- >>> from freealg import FreeForm
1257
- """
1258
-
1259
- eigs = self.eigvalsh(size=size, seed=seed, **kwargs)
1260
- sign = numpy.prod(numpy.sign(eigs))
1261
- ld = numpy.sum(numpy.log(numpy.abs(eigs)))
1262
- return sign, ld
1263
-
1264
- # ====
1265
- # norm
1266
- # ====
1267
-
1268
- def norm(self, size=None, order=2, seed=None, **kwargs):
1269
- """
1270
- Estimate the Schatten norm.
1271
-
1272
- This function estimates the norm of the freeform or a larger
1273
- matrix containing it using free decompression.
1274
-
1275
- Parameters
1276
- ----------
1277
-
1278
- size : int, default=None
1279
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
1280
- eigenvalues of. If None, returns estimates of the eigenvalues of
1281
- :math:`\\mathbf{A}` itself.
1282
-
1283
- order : {float, ``''inf``, ``'-inf'``, ``'fro'``, ``'nuc'``}, default=2
1284
- Order of the norm.
1285
-
1286
- * float :math:`p`: Schatten p-norm.
1287
- * ``'inf'``: Largest absolute eigenvalue
1288
- :math:`\\max \\vert \\lambda_i \\vert)`
1289
- * ``'-inf'``: Smallest absolute eigenvalue
1290
- :math:`\\min \\vert \\lambda_i \\vert)`
1291
- * ``'fro'``: Frobenius norm corresponding to :math:`p=2`
1292
- * ``'nuc'``: Nuclear (or trace) norm corresponding to :math:`p=1`
1293
-
1294
- seed : int, default=None
1295
- The seed for the Quasi-Monte Carlo sampler.
1296
-
1297
- **kwargs : dict, optional
1298
- Pass additional options to the underlying
1299
- :func:`FreeForm.decompress` function.
1300
-
1301
- Returns
1302
- -------
1303
-
1304
- norm : float
1305
- matrix norm
1306
-
1307
- See Also
1308
- --------
1309
-
1310
- FreeForm.eigvalsh
1311
- FreeForm.cond
1312
- FreeForm.slogdet
1313
- FreeForm.trace
1314
-
1315
- Notes
1316
- -----
1317
-
1318
- Thes Schatten :math:`p`-norm is defined by
1319
-
1320
- .. math::
1321
-
1322
- \\Vert \\mathbf{A} \\Vert_p = \\left(
1323
- \\sum_{i=1}^N \\vert \\lambda_i \\vert^p \\right)^{1/p}.
1324
-
1325
- Examples
1326
- --------
1327
-
1328
- .. code-block:: python
1329
- :emphasize-lines: 1
1330
-
1331
- >>> from freealg import FreeForm
1332
- """
1333
-
1334
- eigs = self.eigvalsh(size, seed=seed, **kwargs)
1335
-
1336
- # Check order type and convert to float
1337
- if order == 'nuc':
1338
- order = 1
1339
- elif order == 'fro':
1340
- order = 2
1341
- elif order == 'inf':
1342
- order = float('inf')
1343
- elif order == '-inf':
1344
- order = -float('inf')
1345
- elif not isinstance(order,
1346
- (int, float, numpy.integer, numpy.floating)) \
1347
- and not isinstance(order, (bool, numpy.bool_)):
1348
- raise ValueError('"order" is invalid.')
1349
-
1350
- # Compute norm
1351
- if numpy.isinf(order) and not numpy.isneginf(order):
1352
- norm_ = max(numpy.abs(eigs))
1353
-
1354
- elif numpy.isneginf(order):
1355
- norm_ = min(numpy.abs(eigs))
1356
-
1357
- elif isinstance(order, (int, float, numpy.integer, numpy.floating)) \
1358
- and not isinstance(order, (bool, numpy.bool_)):
1359
- norm_q = numpy.sum(numpy.abs(eigs)**order)
1360
- norm_ = norm_q**(1.0 / order)
1361
-
1362
- return norm_
freealg/_util.py CHANGED
@@ -14,7 +14,7 @@
14
14
  import numpy
15
15
  import scipy
16
16
 
17
- __all__ = ['resolve_complex_dtype', 'compute_eig', 'subsample_matrix']
17
+ __all__ = ['resolve_complex_dtype', 'compute_eig', 'submatrix']
18
18
 
19
19
 
20
20
  # =====================
@@ -72,24 +72,72 @@ def compute_eig(A, lower=False):
72
72
  return eig
73
73
 
74
74
 
75
- # ================
76
- # subsample matrix
77
- # ================
75
+ # =========
76
+ # submatrix
77
+ # =========
78
78
 
79
- def subsample_matrix(matrix, submatrix_size, seed=None):
79
+ def submatrix(matrix, size, paired=True, seed=None):
80
80
  """
81
- Generate a random subsample of a larger matrix
81
+ Randomly sample a submatrix from a larger matrix.
82
+
83
+ Parameters
84
+ ----------
85
+
86
+ matrix : numpy.ndarray
87
+ A 2D square array
88
+
89
+ size : int
90
+ Number of rows and columns of the submatrix
91
+
92
+ paired : bool, default=True
93
+ If `True`, the rows and columns are sampled with the same random
94
+ indices. If `False`, separate random indices are used for selecting
95
+ rows and columns.
96
+
97
+ seed : int, default=None
98
+ Seed for random number generation. If `None`, results will not be
99
+ reproducible.
100
+
101
+ Returns
102
+ -------
103
+
104
+ sub : numpy.ndarray
105
+ A 2D array with the number of rows/columns specified by ``size``.
106
+
107
+ See Also
108
+ --------
109
+
110
+ freealg.sample
111
+
112
+ Examples
113
+ --------
114
+
115
+ .. code-block:: python
116
+ :emphasize-lines: 5
117
+
118
+ >>> import numpy
119
+ >>> from freealg import submatrix
120
+
121
+ >>> A = numpy.random.randn(1000, 1000)
122
+ >>> B = submatrix(A, size=500, paired=True, seed=0)
82
123
  """
83
124
 
84
125
  if matrix.shape[0] != matrix.shape[1]:
85
126
  raise ValueError("Matrix must be square")
86
127
 
87
128
  n = matrix.shape[0]
88
- if submatrix_size > n:
89
- raise ValueError("Submatrix size cannot exceed matrix size")
129
+ if size > n:
130
+ raise ValueError("Submatrix size cannot exceed matrix size.")
90
131
 
91
132
  rng = numpy.random.default_rng(seed)
92
- idx = rng.choice(n, size=submatrix_size, replace=False)
93
- idx = numpy.sort(idx) # optional, preserves original ordering
94
133
 
95
- return matrix[numpy.ix_(idx, idx)]
134
+ idx_row = rng.choice(n, size=size, replace=False)
135
+ idx_row = numpy.sort(idx_row) # optional, preserves original ordering
136
+
137
+ if paired:
138
+ idx_col = idx_row
139
+ else:
140
+ idx_col = rng.choice(n, size=size, replace=False)
141
+ idx_col = numpy.sort(idx_col) # optional, preserves original ordering
142
+
143
+ return matrix[numpy.ix_(idx_row, idx_col)]
@@ -15,7 +15,8 @@ from ._chiral_block import ChiralBlock
15
15
  from ._deformed_wigner import DeformedWigner
16
16
  from ._deformed_marchenko_pastur import DeformedMarchenkoPastur
17
17
  from ._compound_poisson import CompoundPoisson
18
+ from ._fuss_catalan import FussCatalan
18
19
 
19
20
  __all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner',
20
21
  'ChiralBlock', 'DeformedWigner', 'DeformedMarchenkoPastur',
21
- 'CompoundPoisson']
22
+ 'CompoundPoisson', 'FussCatalan']
@@ -0,0 +1,163 @@
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
+ from scipy.interpolate import interp1d
16
+ from ..visualization._plot_util import plot_samples
17
+
18
+ try:
19
+ from scipy.integrate import cumtrapz
20
+ except ImportError:
21
+ from scipy.integrate import cumulative_trapezoid as cumtrapz
22
+ from scipy.stats import qmc
23
+
24
+
25
+ # =================
26
+ # Base Distribution
27
+ # =================
28
+
29
+ class BaseDistribution(object):
30
+ """
31
+ Base class for distributions.
32
+ """
33
+
34
+ # ====
35
+ # init
36
+ # ====
37
+
38
+ def __init__(self):
39
+ """
40
+ """
41
+
42
+ self.lam_m = None
43
+ self.lam_p = None
44
+
45
+ # ======
46
+ # sample
47
+ # ======
48
+
49
+ def sample(self, size, x_min=None, x_max=None, method='qmc', seed=None,
50
+ plot=False, latex=False, save=False):
51
+ """
52
+ Sample from distribution.
53
+
54
+ Parameters
55
+ ----------
56
+
57
+ size : int
58
+ Size of sample.
59
+
60
+ x_min : float, default=None
61
+ Minimum of sample values. If `None`, the left edge of the support
62
+ is used.
63
+
64
+ x_max : float, default=None
65
+ Maximum of sample values. If `None`, the right edge of the support
66
+ is used.
67
+
68
+ method : {``'mc'``, ``'qmc'``}, default= ``'qmc'``
69
+ Method of drawing samples from uniform distribution:
70
+
71
+ * ``'mc'``: Monte Carlo
72
+ * ``'qmc'``: Quasi Monte Carlo
73
+
74
+ seed : int, default=None,
75
+ Seed for random number generator.
76
+
77
+ plot : bool, default=False
78
+ If `True`, samples histogram is plotted.
79
+
80
+ latex : bool, default=False
81
+ If `True`, the plot is rendered using LaTeX. This option is
82
+ relevant only if ``plot=True``.
83
+
84
+ save : bool, default=False
85
+ If not `False`, the plot is saved. If a string is given, it is
86
+ assumed to the save filename (with the file extension). This option
87
+ is relevant only if ``plot=True``.
88
+
89
+ Returns
90
+ -------
91
+
92
+ s : numpy.ndarray
93
+ Samples.
94
+
95
+ Notes
96
+ -----
97
+
98
+ This method uses inverse transform sampling.
99
+
100
+ Examples
101
+ --------
102
+
103
+ .. code-block::python
104
+
105
+ >>> from freealg.distributions import KestenMcKay
106
+ >>> km = KestenMcKay(3)
107
+ >>> s = km.sample(2000)
108
+
109
+ .. image:: ../_static/images/plots/km_samples.png
110
+ :align: center
111
+ :class: custom-dark
112
+ """
113
+
114
+ if x_min is None:
115
+ x_min = self.lam_m
116
+
117
+ if x_max is None:
118
+ x_max = self.lam_p
119
+
120
+ # Grid and PDF
121
+ xs = numpy.linspace(x_min, x_max, size)
122
+ pdf = self.density(xs)
123
+
124
+ # CDF (using cumulative trapezoidal rule)
125
+ cdf = cumtrapz(pdf, xs, initial=0)
126
+ cdf /= cdf[-1] # normalize CDF to 1
127
+
128
+ # Inverse CDF interpolator
129
+ inv_cdf = interp1d(cdf, xs, bounds_error=False,
130
+ fill_value=(x_min, x_max))
131
+
132
+ # Random generator
133
+ rng = numpy.random.default_rng(seed)
134
+
135
+ # Draw from uniform distribution
136
+ if method == 'mc':
137
+ u = rng.random(size)
138
+
139
+ elif method == 'qmc':
140
+ try:
141
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
142
+ except TypeError:
143
+ # Older scipy versions
144
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
145
+ u = engine.random(size).ravel()
146
+
147
+ else:
148
+ raise NotImplementedError('"method" is invalid.')
149
+
150
+ # Draw from distribution by mapping from inverse CDF
151
+ samples = inv_cdf(u).ravel()
152
+
153
+ if plot:
154
+ radius = 0.5 * (self.lam_p - self.lam_m)
155
+ center = 0.5 * (self.lam_p + self.lam_m)
156
+ scale = 1.25
157
+ x_min = numpy.floor(center - radius * scale)
158
+ x_max = numpy.ceil(center + radius * scale)
159
+ x = numpy.linspace(x_min, x_max, 500)
160
+ rho = self.density(x)
161
+ plot_samples(x, rho, x_min, x_max, samples, latex=latex, save=save)
162
+
163
+ return samples