gx2 1.0.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.
gx2/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """gx2 - Generalized chi-square distribution
2
+ ============================================
3
+
4
+ Python port of the MATLAB *Generalized chi-square distribution* toolbox by
5
+ Abhranil Das (Center for Perceptual Systems, The University of Texas at
6
+ Austin). It computes the statistics, characteristic function, pdf, cdf,
7
+ inverse cdf and random numbers of the generalized chi-square distribution --
8
+ the distribution of a weighted sum of non-central chi-square variables plus a
9
+ normal variable, equivalently the quadratic form of a normal vector.
10
+
11
+ A generalized chi-square is parametrised by:
12
+
13
+ w weights of the non-central chi-square terms
14
+ k their degrees of freedom
15
+ lambda_ their non-centralities (named ``lambda_`` since ``lambda`` is a
16
+ Python keyword)
17
+ s scale of the added normal term
18
+ m offset
19
+
20
+ If you use this code, please cite:
21
+ 1. A method to integrate and classify normal distributions
22
+ (https://arxiv.org/abs/2012.14331)
23
+ 2. New methods for computing the generalized chi-square distribution
24
+ (https://arxiv.org/abs/2404.05062)
25
+ """
26
+
27
+ from ._basic import stat, char, rnd
28
+ from ._convert import gx2_to_norm_quad_params, norm_quad_to_gx2_params
29
+ from ._distribution import cdf, pdf, inv, log_cdf
30
+ from ._methods import (imhof, imhof_integrand, ruben, ifft,
31
+ pearson, cdf_pearson, tail, ellipse)
32
+ from ._ray import (cdf_ray, pdf_ray, ray_integrand, int_norm_ray,
33
+ norm_prob_across_rays, norm_prob_across_angles)
34
+ from ._helpers import (log_sum_exp, signed_log_sum_exp, phi_ray,
35
+ Phibar_ray_split, Phibar_sym, prob_ray_sym, standard_quad)
36
+
37
+ __version__ = "1.0.0"
38
+
39
+ __all__ = [
40
+ # core distribution API
41
+ "stat", "char", "rnd", "cdf", "pdf", "inv", "log_cdf",
42
+ # parameter conversions
43
+ "gx2_to_norm_quad_params", "norm_quad_to_gx2_params",
44
+ # individual methods
45
+ "imhof", "imhof_integrand", "ruben", "ifft", "pearson",
46
+ "cdf_pearson", "tail", "ellipse",
47
+ # ray method internals
48
+ "cdf_ray", "pdf_ray", "ray_integrand", "int_norm_ray",
49
+ "norm_prob_across_rays", "norm_prob_across_angles",
50
+ # numerical helpers
51
+ "log_sum_exp", "signed_log_sum_exp", "phi_ray", "Phibar_ray_split",
52
+ "Phibar_sym", "prob_ray_sym", "standard_quad",
53
+ ]
gx2/_basic.py ADDED
@@ -0,0 +1,144 @@
1
+ """Mean/variance, characteristic function, and random number generation.
2
+ Mirrors ``gx2stat.m``, ``gx2char.m`` and ``gx2rnd.m``.
3
+ """
4
+
5
+ import numpy as np
6
+ from scipy.stats import ncx2, chi2, norm
7
+
8
+ from ._helpers import asrow
9
+ from ._convert import gx2_to_norm_quad_params
10
+
11
+
12
+ def stat(w, k, lambda_, s, m):
13
+ """Mean and variance of a generalized chi-square distribution.
14
+
15
+ Parameters
16
+ ----------
17
+ w : array_like
18
+ Weights of the non-central chi-square terms.
19
+ k : array_like
20
+ Degrees of freedom of the non-central chi-square terms.
21
+ lambda_ : array_like
22
+ Non-centrality parameters of the non-central chi-square terms.
23
+ s : float
24
+ Scale (standard deviation) of the added normal term.
25
+ m : float
26
+ Constant offset added to the distribution.
27
+
28
+ Returns
29
+ -------
30
+ mu : float
31
+ Mean.
32
+ v : float
33
+ Variance.
34
+ """
35
+ w = asrow(w)
36
+ k = asrow(k)
37
+ lambda_ = asrow(lambda_)
38
+ mu = float(np.dot(w, k + lambda_) + m)
39
+ v = float(2 * np.dot(w ** 2, k + 2 * lambda_) + s ** 2)
40
+ return mu, v
41
+
42
+
43
+ def char(t, w, k, lambda_, s, m):
44
+ """Characteristic function of a generalized chi-square distribution.
45
+
46
+ Parameters
47
+ ----------
48
+ t : array_like
49
+ Point(s) at which to evaluate the characteristic function.
50
+ w : array_like
51
+ Weights of the non-central chi-square terms.
52
+ k : array_like
53
+ Degrees of freedom of the non-central chi-square terms.
54
+ lambda_ : array_like
55
+ Non-centrality parameters of the non-central chi-square terms.
56
+ s : float
57
+ Scale (standard deviation) of the added normal term.
58
+ m : float
59
+ Constant offset added to the distribution.
60
+
61
+ Returns
62
+ -------
63
+ phi : ndarray of complex
64
+ The characteristic function at each ``t``, shaped like ``t``.
65
+ """
66
+ w = asrow(w)
67
+ k = asrow(k)
68
+ lambda_ = asrow(lambda_)
69
+ t = np.asarray(t, dtype=float)
70
+ tf = t.ravel()
71
+ tc = tf[:, None] # column
72
+
73
+ term = np.sum((w * lambda_) / (1 - 2j * tc * w), axis=1)
74
+ denom = np.prod((1 - 2j * w * tc) ** (k / 2), axis=1)
75
+ phi = np.exp(1j * m * tf + 1j * tf * term - s ** 2 * tf ** 2 / 2) / denom
76
+ return phi.reshape(t.shape)
77
+
78
+
79
+ def rnd(w, k, lambda_, s, m, size=None, method="sum"):
80
+ """Generate generalized chi-square random numbers.
81
+
82
+ Parameters
83
+ ----------
84
+ w : array_like
85
+ Weights of the non-central chi-square terms.
86
+ k : array_like
87
+ Degrees of freedom of the non-central chi-square terms.
88
+ lambda_ : array_like
89
+ Non-centrality parameters of the non-central chi-square terms.
90
+ s : float
91
+ Scale (standard deviation) of the added normal term.
92
+ m : float
93
+ Constant offset added to the distribution.
94
+ size : int or tuple, optional
95
+ Output shape. A scalar ``n`` gives an ``n x n`` array; a tuple gives
96
+ that exact shape. If omitted, a single scalar is returned.
97
+ method : {'sum', 'norm_quad'}
98
+ ``'sum'`` (default) generates non-central chi-square and normal numbers
99
+ and adds them. ``'norm_quad'`` generates standard normal vectors and
100
+ computes their quadratic form.
101
+
102
+ Returns
103
+ -------
104
+ r : float or ndarray
105
+ Random sample(s), of shape ``size`` (or a scalar if ``size`` is None).
106
+ """
107
+ w = asrow(w)
108
+ k = asrow(k)
109
+ lambda_ = asrow(lambda_)
110
+
111
+ if size is None:
112
+ shape = ()
113
+ elif np.isscalar(size):
114
+ shape = (int(size), int(size))
115
+ else:
116
+ shape = tuple(int(x) for x in size)
117
+
118
+ method = str(method).lower()
119
+ if method == "sum":
120
+ r = np.zeros(shape)
121
+ for wi, ki, li in zip(w, k, lambda_):
122
+ if li == 0:
123
+ r = r + wi * chi2.rvs(df=ki, size=shape)
124
+ else:
125
+ r = r + wi * ncx2.rvs(df=ki, nc=li, size=shape)
126
+ if s:
127
+ r = r + norm.rvs(loc=m, scale=s, size=shape)
128
+ else:
129
+ r = r + m
130
+ return r
131
+ elif method == "norm_quad":
132
+ quad = gx2_to_norm_quad_params(w, k, lambda_, s, m)
133
+ q1 = np.asarray(quad["q1"]).ravel()
134
+ q2 = np.asarray(quad["q2"])
135
+ q0 = quad["q0"]
136
+ dim = q1.size
137
+ n = int(np.prod(shape)) if shape else 1
138
+ z = norm.rvs(loc=0, scale=1, size=(dim, n))
139
+ r = np.sum(z * (q2 @ z), axis=0) + q1 @ z + q0
140
+ if shape:
141
+ return r.reshape(shape)
142
+ return float(r[0])
143
+ else:
144
+ raise ValueError("method must be 'sum' or 'norm_quad'")
gx2/_convert.py ADDED
@@ -0,0 +1,106 @@
1
+ """Conversions between generalized chi-square parameters and the quadratic form
2
+ of a normal vector. Mirrors ``gx2_to_norm_quad_params.m`` and
3
+ ``norm_quad_to_gx2_params.m``.
4
+ """
5
+
6
+ import numpy as np
7
+ from ._helpers import asrow, uniquetol
8
+
9
+
10
+ def gx2_to_norm_quad_params(w, k, lambda_, s, m):
11
+ """Quadratic-form coefficients of the standard normal whose quadratic form
12
+ is the given generalized chi-square.
13
+
14
+ Parameters
15
+ ----------
16
+ w, k, lambda_ : array_like
17
+ Weights, degrees of freedom and non-centralities of the non-central
18
+ chi-square terms.
19
+ s : float
20
+ Scale of the normal term.
21
+ m : float
22
+ Offset.
23
+
24
+ Returns
25
+ -------
26
+ quad : dict
27
+ ``{'q2': matrix, 'q1': vector, 'q0': scalar}``. The dimension of the
28
+ standard normal is ``len(q1)``.
29
+ """
30
+ w = asrow(w)
31
+ k = asrow(k)
32
+ lambda_ = asrow(lambda_)
33
+
34
+ q2_parts = []
35
+ q1_parts = []
36
+ for wi, ki, li in zip(w, k, lambda_):
37
+ ki = int(round(ki))
38
+ q2_parts.append(np.full(ki, wi))
39
+ q1_parts.append(np.concatenate(([wi * np.sqrt(li)], np.zeros(ki - 1))))
40
+ q2 = np.concatenate(q2_parts) if q2_parts else np.array([])
41
+ q1 = -2 * (np.concatenate(q1_parts) if q1_parts else np.array([]))
42
+
43
+ if s:
44
+ q2 = np.append(q2, 0.0)
45
+ q1 = np.append(q1, s)
46
+
47
+ return {"q2": np.diag(q2), "q1": q1.astype(float), "q0": float(np.dot(w, lambda_) + m)}
48
+
49
+
50
+ def norm_quad_to_gx2_params(mu, v, quad, merge=True):
51
+ """Parameters of the generalized chi-square distribution of a quadratic
52
+ form ``q(x) = x' q2 x + q1' x + q0`` of a normal vector ``x ~ N(mu, v)``.
53
+
54
+ Parameters
55
+ ----------
56
+ mu : array_like
57
+ Column vector of the normal mean.
58
+ v : array_like
59
+ Normal covariance matrix.
60
+ quad : dict
61
+ ``{'q2': matrix, 'q1': vector, 'q0': scalar}``.
62
+ merge : bool, optional
63
+ If True (default), merge non-central chi-square components with
64
+ close-enough weights into single components. Set False to return all
65
+ raw exact components.
66
+
67
+ Returns
68
+ -------
69
+ w, k, lambda_, s, m
70
+ """
71
+ mu = np.asarray(mu, dtype=float).ravel()
72
+ v = np.asarray(v, dtype=float)
73
+ q2_in = np.asarray(quad["q2"], dtype=float)
74
+ q1_in = np.asarray(quad["q1"], dtype=float).ravel()
75
+ q0_in = float(quad["q0"])
76
+
77
+ q2_sym = 0.5 * (q2_in + q2_in.T)
78
+
79
+ # sqrtm(v) avoiding small negative eigenvalues
80
+ d, R = np.linalg.eigh(v)
81
+ d = np.where(d < 0, 0.0, d)
82
+ sqrt_v = R @ np.diag(np.sqrt(d)) @ R.T
83
+
84
+ q2 = sqrt_v @ q2_sym @ sqrt_v
85
+ q2 = (q2 + q2.T) / 2
86
+ q1 = sqrt_v @ (2 * q2_sym @ mu + q1_in)
87
+ q0 = float(mu @ q2_sym @ mu + q1_in @ mu + q0_in)
88
+
89
+ d2, R2 = np.linalg.eigh(q2)
90
+ d = d2
91
+ b = (R2.T @ q1)
92
+
93
+ nz = d != 0
94
+ if merge:
95
+ w, ic = uniquetol(d[nz])
96
+ k = np.bincount(ic, minlength=w.size).astype(float)
97
+ b_sq_sum = np.bincount(ic, weights=b[nz] ** 2, minlength=w.size)
98
+ lambda_ = b_sq_sum / (4 * w ** 2)
99
+ else:
100
+ w = d[nz].copy()
101
+ k = np.ones(w.size)
102
+ lambda_ = b[nz] ** 2 / (4 * w ** 2)
103
+
104
+ m = q0 - np.dot(w, lambda_)
105
+ s = np.linalg.norm(b[~nz])
106
+ return w, k, lambda_, s, m
gx2/_distribution.py ADDED
@@ -0,0 +1,363 @@
1
+ """Top-level cdf, pdf and inverse-cdf with automatic method selection.
2
+ Mirrors gx2cdf.m, gx2pdf.m, gx2inv.m and log_gx2cdf.m.
3
+ """
4
+
5
+ import inspect
6
+ import numpy as np
7
+ from scipy.stats import norm
8
+
9
+ from ._helpers import asrow, fzero
10
+ from ._basic import stat
11
+ from ._methods import (imhof, ruben, ifft, pearson, tail,
12
+ ellipse, _ncx2cdf)
13
+ from ._ray import cdf_ray, pdf_ray, int_norm_ray
14
+
15
+
16
+ def _filter(func, kwargs):
17
+ """Keep only kwargs accepted by ``func`` (mimics MATLAB KeepUnmatched)."""
18
+ params = inspect.signature(func).parameters
19
+ return {k: v for k, v in kwargs.items() if k in params}
20
+
21
+
22
+ def _is_full(x):
23
+ return isinstance(x, str) and x.lower() == "full"
24
+
25
+
26
+ # ===========================================================================
27
+ # cdf
28
+ # ===========================================================================
29
+
30
+ def cdf(x, w, k, lambda_, s, m, side="lower", method="auto",
31
+ full_output=False, **kwargs):
32
+ """Cdf of a generalized chi-square distribution.
33
+
34
+ Parameters
35
+ ----------
36
+ x : float, array_like, or 'full'
37
+ Point(s) at which to evaluate the cdf. The output has the same shape as
38
+ ``x``. Pass the string ``'full'`` to evaluate the cdf over a grid that
39
+ automatically spans the whole distribution; the grid is then returned
40
+ as ``x_grid`` (see Returns), so use ``'full'`` together with the
41
+ returned tuple.
42
+ w : array_like
43
+ Weights of the non-central chi-square terms.
44
+ k : array_like
45
+ Degrees of freedom of the non-central chi-square terms.
46
+ lambda_ : array_like
47
+ Non-centrality parameters of the non-central chi-square terms (one per
48
+ term, same length as ``w`` and ``k``).
49
+ s : float
50
+ Scale (standard deviation) of the added normal term.
51
+ m : float
52
+ Constant offset added to the distribution.
53
+ side : {'lower', 'upper'}
54
+ ``'lower'`` (default) returns the cdf P(X <= x). ``'upper'`` returns the
55
+ complementary cdf P(X > x), computed in a way that stays accurate when
56
+ it is very small.
57
+ method : {'auto','imhof','ray','ifft','ruben','tail','pearson','ellipse'}
58
+ Algorithm used. ``'auto'`` (default) picks a suitable one for the given
59
+ parameters. The others let you choose explicitly; some have constraints
60
+ (e.g. ``'ruben'`` and ``'ellipse'`` need all ``w`` the same sign and
61
+ ``s == 0``).
62
+ full_output : bool
63
+ Controls how many values are returned (see Returns). Defaults to False,
64
+ and is forced True when ``x='full'``.
65
+
66
+ Returns
67
+ -------
68
+ p : float or ndarray
69
+ The cdf (or complementary cdf if ``side='upper'``) at each ``x``, shaped
70
+ like ``x``. This is the sole return value when ``full_output`` is False.
71
+ With the ``'tail'``, ``'ellipse'`` and ``'ray'`` methods, any entry that
72
+ is too small for double precision (below ~1e-308) is returned instead as
73
+ its base-10 logarithm, which is negative; entries that fit normally stay
74
+ positive. This lets the far tails be represented down to arbitrarily
75
+ tiny probabilities.
76
+ p_err : ndarray or None
77
+ Returned only when ``full_output=True``. An estimate of the numerical
78
+ error in ``p`` (its exact meaning depends on the method, e.g. the
79
+ Monte-Carlo standard error for ``'ray'`` or an error bound for
80
+ ``'ruben'``), or ``None`` for methods that do not provide one.
81
+ x_grid : ndarray or None
82
+ Returned only when ``full_output=True``. When ``x='full'``, the array of
83
+ points at which ``p`` was evaluated; otherwise ``None``.
84
+
85
+ Examples
86
+ --------
87
+ >>> import gx2
88
+ >>> gx2.cdf(25, [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5) # cdf at x=25
89
+ >>> gx2.cdf(25, [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5, side='upper')
90
+ >>> p, p_err, x_grid = gx2.cdf('full', [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5,
91
+ ... full_output=True)
92
+ """
93
+ w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
94
+ full = _is_full(x)
95
+ if not full:
96
+ x = np.asarray(x, dtype=float)
97
+ p_err = None
98
+ x_grid = None
99
+
100
+ if full:
101
+ method = "ifft"
102
+
103
+ if method == "auto":
104
+ uw = np.unique(w)
105
+ if (not s) and uw.size == 1:
106
+ uw0 = uw[0]
107
+ lower = ((np.sign(uw0) == 1 and side == "lower")
108
+ or (np.sign(uw0) == -1 and side == "upper"))
109
+ p = _ncx2cdf((x - m) / uw0, np.sum(k), np.sum(lambda_), upper=not lower)
110
+ elif np.sum(np.abs(w)) == 0 and s:
111
+ p = norm.sf(x, m, s) if side == "upper" else norm.cdf(x, m, s)
112
+ elif not s:
113
+ if (np.all(w > 0) and side == "lower") or (np.all(w < 0) and side == "upper"):
114
+ try:
115
+ p, p_err = ruben(x, w, k, lambda_, m, side=side,
116
+ **_filter(ruben, kwargs))
117
+ except Exception:
118
+ p, p_err = imhof(x, w, k, lambda_, 0, m, side=side,
119
+ **_filter(imhof, kwargs))
120
+ else:
121
+ p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
122
+ **_filter(imhof, kwargs))
123
+ else:
124
+ p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
125
+ **_filter(imhof, kwargs))
126
+ elif method == "ifft":
127
+ p, x_grid = ifft(x, w, k, lambda_, s, m, side=side, output="cdf",
128
+ **_filter(ifft, kwargs))
129
+ elif method == "ray":
130
+ p, p_err = cdf_ray(x, w, k, lambda_, s, m, side=side,
131
+ **_filter(int_norm_ray, kwargs))
132
+ elif method == "imhof":
133
+ p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
134
+ **_filter(imhof, kwargs))
135
+ elif method == "ruben":
136
+ if s or not (np.all(w > 0) or np.all(w < 0)):
137
+ raise ValueError("Ruben's method can only be used when all w are "
138
+ "the same sign and s=0.")
139
+ p, p_err = ruben(x, w, k, lambda_, m, side=side,
140
+ **_filter(ruben, kwargs))
141
+ elif method == "tail":
142
+ p = tail(x, w, k, lambda_, s, m, side=side, **_filter(tail, kwargs))
143
+ elif method == "pearson":
144
+ p = pearson(x, w, k, lambda_, s, m, side=side, **_filter(pearson, kwargs))
145
+ elif method == "ellipse":
146
+ if s or not (np.all(w > 0) or np.all(w < 0)):
147
+ raise ValueError("The ellipse approximation can only be used when "
148
+ "all w are the same sign and s=0.")
149
+ p, p_err = ellipse(x, w, k, lambda_, m, side=side,
150
+ **_filter(ellipse, kwargs))
151
+ else:
152
+ raise ValueError("unknown method %r" % method)
153
+
154
+ if full or full_output:
155
+ return p, p_err, x_grid
156
+ return p
157
+
158
+
159
+ # ===========================================================================
160
+ # pdf
161
+ # ===========================================================================
162
+
163
+ def pdf(x, w, k, lambda_, s, m, side="lower", method="auto", diff=False,
164
+ dx=None, full_output=False, **kwargs):
165
+ """Pdf of a generalized chi-square distribution.
166
+
167
+ Parameters
168
+ ----------
169
+ x : float, array_like, or 'full'
170
+ Point(s) at which to evaluate the pdf; output is shaped like ``x``.
171
+ ``'full'`` evaluates over an automatically chosen spanning grid, which
172
+ is returned as ``x_grid`` (see Returns).
173
+ w : array_like
174
+ Weights of the non-central chi-square terms.
175
+ k : array_like
176
+ Degrees of freedom of the non-central chi-square terms.
177
+ lambda_ : array_like
178
+ Non-centrality parameters of the non-central chi-square terms.
179
+ s : float
180
+ Scale (standard deviation) of the added normal term.
181
+ m : float
182
+ Constant offset added to the distribution.
183
+ side : {'lower', 'upper'}
184
+ Only affects the ``'tail'`` method, selecting which infinite tail to
185
+ approximate.
186
+ method : {'auto','imhof','ray','ifft','ruben','tail','pearson','ellipse'}
187
+ Algorithm used; ``'auto'`` (default) picks a suitable one.
188
+ diff : bool
189
+ If True, obtain the pdf by numerically differentiating :func:`cdf`
190
+ instead of evaluating it directly.
191
+ dx : float, optional
192
+ Step size for the ``diff=True`` finite difference. Defaults to the
193
+ distribution's standard deviation divided by 1e4.
194
+ full_output : bool
195
+ Controls how many values are returned (see Returns). Defaults to False,
196
+ and is forced True when ``x='full'``.
197
+
198
+ Returns
199
+ -------
200
+ f : float or ndarray
201
+ The pdf at each ``x``, shaped like ``x``. This is the sole return value
202
+ when ``full_output`` is False. As with :func:`cdf`, the ``'tail'``,
203
+ ``'ellipse'`` and ``'ray'`` methods return the (negative) base-10
204
+ logarithm for entries too small for double precision.
205
+ f_err : ndarray or None
206
+ Returned only when ``full_output=True``. A method-dependent estimate of
207
+ the numerical error in ``f``, or ``None`` if unavailable.
208
+ x_grid : ndarray or None
209
+ Returned only when ``full_output=True``. The grid of points used when
210
+ ``x='full'``; otherwise ``None``.
211
+ """
212
+ w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
213
+ full = _is_full(x)
214
+ if not full:
215
+ x = np.asarray(x, dtype=float)
216
+ f_err = None
217
+ x_grid = None
218
+
219
+ if full:
220
+ method = "ifft"
221
+
222
+ if not diff:
223
+ if method == "auto":
224
+ uw = np.unique(w)
225
+ if (not s) and uw.size == 1 and not full:
226
+ from ._methods import _ncx2pdf
227
+ f = _ncx2pdf((x - m) / uw[0], np.sum(k), np.sum(lambda_)) / abs(uw[0])
228
+ elif np.sum(np.abs(w)) == 0 and s:
229
+ f = norm.pdf(x, m, s)
230
+ else:
231
+ f, _ = imhof(x, w, k, lambda_, s, m, output="pdf",
232
+ **_filter(imhof, kwargs))
233
+ elif method == "imhof":
234
+ f, _ = imhof(x, w, k, lambda_, s, m, output="pdf",
235
+ **_filter(imhof, kwargs))
236
+ elif method == "ruben":
237
+ if s or not (np.all(w > 0) or np.all(w < 0)):
238
+ raise ValueError("Ruben's method can only be used when all w are "
239
+ "the same sign and s=0.")
240
+ f, _ = ruben(x, w, k, lambda_, m, output="pdf",
241
+ **_filter(ruben, kwargs))
242
+ elif method == "tail":
243
+ f = tail(x, w, k, lambda_, s, m, side=side, output="pdf",
244
+ **_filter(tail, kwargs))
245
+ elif method == "pearson":
246
+ f = pearson(x, w, k, lambda_, s, m, side=side, output="pdf",
247
+ **_filter(pearson, kwargs))
248
+ elif method == "ellipse":
249
+ if s or not (np.all(w > 0) or np.all(w < 0)):
250
+ raise ValueError("The ellipse approximation can only be used when "
251
+ "all w are the same sign and s=0.")
252
+ f, f_err = ellipse(x, w, k, lambda_, m, side=side, output="pdf",
253
+ **_filter(ellipse, kwargs))
254
+ elif method == "ray":
255
+ f, f_err = pdf_ray(x, w, k, lambda_, s, m,
256
+ **_filter(pdf_ray, kwargs))
257
+ elif method == "ifft":
258
+ f, x_grid = ifft(x, w, k, lambda_, s, m, side=side, output="pdf",
259
+ **_filter(ifft, kwargs))
260
+ else:
261
+ raise ValueError("unknown method %r" % method)
262
+ else:
263
+ if dx is None:
264
+ _, v = stat(w, k, lambda_, s, m)
265
+ dx = np.sqrt(v) / 1e4
266
+ p_left = cdf(x - dx, w, k, lambda_, s, m, side=side, method=method, **kwargs)
267
+ p_right = cdf(x + dx, w, k, lambda_, s, m, side=side, method=method, **kwargs)
268
+ f = (p_right - p_left) / (2 * dx)
269
+ f = np.maximum(f, 0)
270
+
271
+ if full or full_output:
272
+ return f, f_err, x_grid
273
+ return f
274
+
275
+
276
+ # ===========================================================================
277
+ # inverse cdf
278
+ # ===========================================================================
279
+
280
+ def inv(p, w, k, lambda_, s, m, side="lower", method="auto", **kwargs):
281
+ """Inverse cdf (quantile function) of a generalized chi-square distribution.
282
+
283
+ Parameters
284
+ ----------
285
+ p : float or array_like
286
+ Probability or probabilities at which to evaluate the quantile, in
287
+ (0, 1]. A negative value is interpreted as the base-10 logarithm of the
288
+ probability, which lets you request quantiles for probabilities below
289
+ ~1e-308 (e.g. ``p=-1000`` means a cdf of 1e-1000); pair this with a
290
+ forward method that reaches such tiny values, e.g. ``method='tail'``,
291
+ ``'ellipse'`` or ``'ray'``.
292
+ w : array_like
293
+ Weights of the non-central chi-square terms.
294
+ k : array_like
295
+ Degrees of freedom of the non-central chi-square terms.
296
+ lambda_ : array_like
297
+ Non-centrality parameters of the non-central chi-square terms.
298
+ s : float
299
+ Scale (standard deviation) of the added normal term.
300
+ m : float
301
+ Constant offset added to the distribution.
302
+ side : {'lower', 'upper'}
303
+ ``'upper'`` inverts the complementary cdf, i.e. ``p`` is a tail
304
+ probability.
305
+ method : str
306
+ Forward cdf method used while root-finding; see :func:`cdf`.
307
+
308
+ Returns
309
+ -------
310
+ x : float or ndarray
311
+ The quantile(s): the value(s) of x at which the cdf equals ``p``.
312
+ Returns a scalar for scalar ``p``, otherwise an array shaped like ``p``.
313
+ """
314
+ w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
315
+ p = np.atleast_1d(np.asarray(p, dtype=float))
316
+
317
+ uw = np.unique(w)
318
+ if (not s) and uw.size == 1 and np.all(p > 0):
319
+ uw0 = uw[0]
320
+ pp = 1 - p if side == "upper" else p
321
+ from scipy.stats import ncx2, chi2
322
+ df = np.sum(k); nc = np.sum(lambda_)
323
+
324
+ def _ncx2inv(pr):
325
+ if nc == 0:
326
+ return chi2.ppf(pr, df)
327
+ return ncx2.ppf(pr, df, nc)
328
+
329
+ if np.sign(uw0) == 1:
330
+ x = _ncx2inv(pp) * uw0 + m
331
+ elif np.sign(uw0) == -1:
332
+ x = _ncx2inv(1 - pp) * uw0 + m
333
+ else:
334
+ x = np.zeros_like(pp)
335
+ else:
336
+ mu, _ = stat(w, k, lambda_, s, m)
337
+
338
+ def solve_one(pi):
339
+ if pi > 0:
340
+ f = lambda xx: cdf(xx, w, k, lambda_, s, m, side=side,
341
+ method=method, **kwargs) - pi
342
+ else:
343
+ f = lambda xx: log_cdf(xx, w, k, lambda_, s, m, side=side,
344
+ method=method, **kwargs) - pi
345
+ return fzero(f, mu)
346
+
347
+ x = np.array([solve_one(pi) for pi in p])
348
+
349
+ x = np.asarray(x)
350
+ if x.size == 1:
351
+ return float(x.ravel()[0])
352
+ return x
353
+
354
+
355
+ def log_cdf(x, w, k, lambda_, s, m, **kwargs):
356
+ """log10 of the cdf, returning the (negative) value itself when the cdf
357
+ has already underflowed to a log10 value."""
358
+ p = cdf(x, w, k, lambda_, s, m, **kwargs)
359
+ p = float(np.asarray(p).ravel()[0]) if np.size(p) == 1 else p
360
+ if np.isscalar(p) or np.ndim(p) == 0:
361
+ return p if p <= 0 else np.log10(p)
362
+ p = np.asarray(p, dtype=float)
363
+ return np.where(p <= 0, p, np.log10(np.where(p > 0, p, 1)))