Coverage for /usr/lib/python3/dist-packages/scipy/stats/_qmvnt.py: 6%

250 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1# Integration of multivariate normal and t distributions. 

2 

3# Adapted from the MATLAB original implementations by Dr. Alan Genz. 

4 

5# http://www.math.wsu.edu/faculty/genz/software/software.html 

6 

7# Copyright (C) 2013, Alan Genz, All rights reserved. 

8# Python implementation is copyright (C) 2022, Robert Kern, All rights 

9# reserved. 

10 

11# Redistribution and use in source and binary forms, with or without 

12# modification, are permitted provided the following conditions are met: 

13# 1. Redistributions of source code must retain the above copyright 

14# notice, this list of conditions and the following disclaimer. 

15# 2. Redistributions in binary form must reproduce the above copyright 

16# notice, this list of conditions and the following disclaimer in 

17# the documentation and/or other materials provided with the 

18# distribution. 

19# 3. The contributor name(s) may not be used to endorse or promote 

20# products derived from this software without specific prior 

21# written permission. 

22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 

23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 

24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 

25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 

26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 

27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 

28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 

29# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 

30# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 

31# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF USE 

32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

33 

34 

35import numpy as np 

36 

37from scipy.fft import fft, ifft 

38from scipy.special import gammaincinv, ndtr, ndtri 

39from scipy.stats._qmc import primes_from_2_to 

40 

41 

42phi = ndtr 

43phinv = ndtri 

44 

45 

46def _factorize_int(n): 

47 """Return a sorted list of the unique prime factors of a positive integer. 

48 """ 

49 # NOTE: There are lots faster ways to do this, but this isn't terrible. 

50 factors = set() 

51 for p in primes_from_2_to(int(np.sqrt(n)) + 1): 

52 while not (n % p): 

53 factors.add(p) 

54 n //= p 

55 if n == 1: 

56 break 

57 if n != 1: 

58 factors.add(n) 

59 return sorted(factors) 

60 

61 

62def _primitive_root(p): 

63 """Compute a primitive root of the prime number `p`. 

64 

65 Used in the CBC lattice construction. 

66 

67 References 

68 ---------- 

69 .. [1] https://en.wikipedia.org/wiki/Primitive_root_modulo_n 

70 """ 

71 # p is prime 

72 pm = p - 1 

73 factors = _factorize_int(pm) 

74 n = len(factors) 

75 r = 2 

76 k = 0 

77 while k < n: 

78 d = pm // factors[k] 

79 # pow() doesn't like numpy scalar types. 

80 rd = pow(int(r), int(d), int(p)) 

81 if rd == 1: 

82 r += 1 

83 k = 0 

84 else: 

85 k += 1 

86 return r 

87 

88 

89def _cbc_lattice(n_dim, n_qmc_samples): 

90 """Compute a QMC lattice generator using a Fast CBC construction. 

91 

92 Parameters 

93 ---------- 

94 n_dim : int > 0 

95 The number of dimensions for the lattice. 

96 n_qmc_samples : int > 0 

97 The desired number of QMC samples. This will be rounded down to the 

98 nearest prime to enable the CBC construction. 

99 

100 Returns 

101 ------- 

102 q : float array : shape=(n_dim,) 

103 The lattice generator vector. All values are in the open interval 

104 `(0, 1)`. 

105 actual_n_qmc_samples : int 

106 The prime number of QMC samples that must be used with this lattice, 

107 no more, no less. 

108 

109 References 

110 ---------- 

111 .. [1] Nuyens, D. and Cools, R. "Fast Component-by-Component Construction, 

112 a Reprise for Different Kernels", In H. Niederreiter and D. Talay, 

113 editors, Monte-Carlo and Quasi-Monte Carlo Methods 2004, 

114 Springer-Verlag, 2006, 371-385. 

115 """ 

116 # Round down to the nearest prime number. 

117 primes = primes_from_2_to(n_qmc_samples + 1) 

118 n_qmc_samples = primes[-1] 

119 

120 bt = np.ones(n_dim) 

121 gm = np.hstack([1.0, 0.8 ** np.arange(n_dim - 1)]) 

122 q = 1 

123 w = 0 

124 z = np.arange(1, n_dim + 1) 

125 m = (n_qmc_samples - 1) // 2 

126 g = _primitive_root(n_qmc_samples) 

127 # Slightly faster way to compute perm[j] = pow(g, j, n_qmc_samples) 

128 # Shame that we don't have modulo pow() implemented as a ufunc. 

129 perm = np.ones(m, dtype=int) 

130 for j in range(m - 1): 

131 perm[j + 1] = (g * perm[j]) % n_qmc_samples 

132 perm = np.minimum(n_qmc_samples - perm, perm) 

133 pn = perm / n_qmc_samples 

134 c = pn * pn - pn + 1.0 / 6 

135 fc = fft(c) 

136 for s in range(1, n_dim): 

137 reordered = np.hstack([ 

138 c[:w+1][::-1], 

139 c[w+1:m][::-1], 

140 ]) 

141 q = q * (bt[s-1] + gm[s-1] * reordered) 

142 w = ifft(fc * fft(q)).real.argmin() 

143 z[s] = perm[w] 

144 q = z / n_qmc_samples 

145 return q, n_qmc_samples 

146 

147 

148# Note: this function is not currently used or tested by any SciPy code. It is 

149# included in this file to facilitate the development of a parameter for users 

150# to set the desired CDF accuracy, but must be reviewed and tested before use. 

151def _qauto(func, covar, low, high, rng, error=1e-3, limit=10_000, **kwds): 

152 """Automatically rerun the integration to get the required error bound. 

153 

154 Parameters 

155 ---------- 

156 func : callable 

157 Either :func:`_qmvn` or :func:`_qmvt`. 

158 covar, low, high : array 

159 As specified in :func:`_qmvn` and :func:`_qmvt`. 

160 rng : Generator, optional 

161 default_rng(), yada, yada 

162 error : float > 0 

163 The desired error bound. 

164 limit : int > 0: 

165 The rough limit of the number of integration points to consider. The 

166 integration will stop looping once this limit has been *exceeded*. 

167 **kwds : 

168 Other keyword arguments to pass to `func`. When using :func:`_qmvt`, be 

169 sure to include ``nu=`` as one of these. 

170 

171 Returns 

172 ------- 

173 prob : float 

174 The estimated probability mass within the bounds. 

175 est_error : float 

176 3 times the standard error of the batch estimates. 

177 n_samples : int 

178 The number of integration points actually used. 

179 """ 

180 n = len(covar) 

181 n_samples = 0 

182 if n == 1: 

183 prob = phi(high) - phi(low) 

184 # More or less 

185 est_error = 1e-15 

186 else: 

187 mi = min(limit, n * 1000) 

188 prob = 0.0 

189 est_error = 1.0 

190 ei = 0.0 

191 while est_error > error and n_samples < limit: 

192 mi = round(np.sqrt(2) * mi) 

193 pi, ei, ni = func(mi, covar, low, high, rng=rng, **kwds) 

194 n_samples += ni 

195 wt = 1.0 / (1 + (ei / est_error)**2) 

196 prob += wt * (pi - prob) 

197 est_error = np.sqrt(wt) * ei 

198 return prob, est_error, n_samples 

199 

200 

201# Note: this function is not currently used or tested by any SciPy code. It is 

202# included in this file to facilitate the resolution of gh-8367, gh-16142, and 

203# possibly gh-14286, but must be reviewed and tested before use. 

204def _qmvn(m, covar, low, high, rng, lattice='cbc', n_batches=10): 

205 """Multivariate normal integration over box bounds. 

206 

207 Parameters 

208 ---------- 

209 m : int > n_batches 

210 The number of points to sample. This number will be divided into 

211 `n_batches` batches that apply random offsets of the sampling lattice 

212 for each batch in order to estimate the error. 

213 covar : (n, n) float array 

214 Possibly singular, positive semidefinite symmetric covariance matrix. 

215 low, high : (n,) float array 

216 The low and high integration bounds. 

217 rng : Generator, optional 

218 default_rng(), yada, yada 

219 lattice : 'cbc' or callable 

220 The type of lattice rule to use to construct the integration points. 

221 n_batches : int > 0, optional 

222 The number of QMC batches to apply. 

223 

224 Returns 

225 ------- 

226 prob : float 

227 The estimated probability mass within the bounds. 

228 est_error : float 

229 3 times the standard error of the batch estimates. 

230 """ 

231 cho, lo, hi = _permuted_cholesky(covar, low, high) 

232 n = cho.shape[0] 

233 ct = cho[0, 0] 

234 c = phi(lo[0] / ct) 

235 d = phi(hi[0] / ct) 

236 ci = c 

237 dci = d - ci 

238 prob = 0.0 

239 error_var = 0.0 

240 q, n_qmc_samples = _cbc_lattice(n - 1, max(m // n_batches, 1)) 

241 y = np.zeros((n - 1, n_qmc_samples)) 

242 i_samples = np.arange(n_qmc_samples) + 1 

243 for j in range(n_batches): 

244 c = np.full(n_qmc_samples, ci) 

245 dc = np.full(n_qmc_samples, dci) 

246 pv = dc.copy() 

247 for i in range(1, n): 

248 # Pseudorandomly-shifted lattice coordinate. 

249 z = q[i - 1] * i_samples + rng.random() 

250 # Fast remainder(z, 1.0) 

251 z -= z.astype(int) 

252 # Tent periodization transform. 

253 x = abs(2 * z - 1) 

254 y[i - 1, :] = phinv(c + x * dc) 

255 s = cho[i, :i] @ y[:i, :] 

256 ct = cho[i, i] 

257 c = phi((lo[i] - s) / ct) 

258 d = phi((hi[i] - s) / ct) 

259 dc = d - c 

260 pv = pv * dc 

261 # Accumulate the mean and error variances with online formulations. 

262 d = (pv.mean() - prob) / (j + 1) 

263 prob += d 

264 error_var = (j - 1) * error_var / (j + 1) + d * d 

265 # Error bounds are 3 times the standard error of the estimates. 

266 est_error = 3 * np.sqrt(error_var) 

267 n_samples = n_qmc_samples * n_batches 

268 return prob, est_error, n_samples 

269 

270 

271# Note: this function is not currently used or tested by any SciPy code. It is 

272# included in this file to facilitate the resolution of gh-8367, gh-16142, and 

273# possibly gh-14286, but must be reviewed and tested before use. 

274def _mvn_qmc_integrand(covar, low, high, use_tent=False): 

275 """Transform the multivariate normal integration into a QMC integrand over 

276 a unit hypercube. 

277 

278 The dimensionality of the resulting hypercube integration domain is one 

279 less than the dimensionality of the original integrand. Note that this 

280 transformation subsumes the integration bounds in order to account for 

281 infinite bounds. The QMC integration one does with the returned integrand 

282 should be on the unit hypercube. 

283 

284 Parameters 

285 ---------- 

286 covar : (n, n) float array 

287 Possibly singular, positive semidefinite symmetric covariance matrix. 

288 low, high : (n,) float array 

289 The low and high integration bounds. 

290 use_tent : bool, optional 

291 If True, then use tent periodization. Only helpful for lattice rules. 

292 

293 Returns 

294 ------- 

295 integrand : Callable[[NDArray], NDArray] 

296 The QMC-integrable integrand. It takes an 

297 ``(n_qmc_samples, ndim_integrand)`` array of QMC samples in the unit 

298 hypercube and returns the ``(n_qmc_samples,)`` evaluations of at these 

299 QMC points. 

300 ndim_integrand : int 

301 The dimensionality of the integrand. Equal to ``n-1``. 

302 """ 

303 cho, lo, hi = _permuted_cholesky(covar, low, high) 

304 n = cho.shape[0] 

305 ndim_integrand = n - 1 

306 ct = cho[0, 0] 

307 c = phi(lo[0] / ct) 

308 d = phi(hi[0] / ct) 

309 ci = c 

310 dci = d - ci 

311 

312 def integrand(*zs): 

313 ndim_qmc = len(zs) 

314 n_qmc_samples = len(np.atleast_1d(zs[0])) 

315 assert ndim_qmc == ndim_integrand 

316 y = np.zeros((ndim_qmc, n_qmc_samples)) 

317 c = np.full(n_qmc_samples, ci) 

318 dc = np.full(n_qmc_samples, dci) 

319 pv = dc.copy() 

320 for i in range(1, n): 

321 if use_tent: 

322 # Tent periodization transform. 

323 x = abs(2 * zs[i-1] - 1) 

324 else: 

325 x = zs[i-1] 

326 y[i - 1, :] = phinv(c + x * dc) 

327 s = cho[i, :i] @ y[:i, :] 

328 ct = cho[i, i] 

329 c = phi((lo[i] - s) / ct) 

330 d = phi((hi[i] - s) / ct) 

331 dc = d - c 

332 pv = pv * dc 

333 return pv 

334 

335 return integrand, ndim_integrand 

336 

337 

338def _qmvt(m, nu, covar, low, high, rng, lattice='cbc', n_batches=10): 

339 """Multivariate t integration over box bounds. 

340 

341 Parameters 

342 ---------- 

343 m : int > n_batches 

344 The number of points to sample. This number will be divided into 

345 `n_batches` batches that apply random offsets of the sampling lattice 

346 for each batch in order to estimate the error. 

347 nu : float >= 0 

348 The shape parameter of the multivariate t distribution. 

349 covar : (n, n) float array 

350 Possibly singular, positive semidefinite symmetric covariance matrix. 

351 low, high : (n,) float array 

352 The low and high integration bounds. 

353 rng : Generator, optional 

354 default_rng(), yada, yada 

355 lattice : 'cbc' or callable 

356 The type of lattice rule to use to construct the integration points. 

357 n_batches : int > 0, optional 

358 The number of QMC batches to apply. 

359 

360 Returns 

361 ------- 

362 prob : float 

363 The estimated probability mass within the bounds. 

364 est_error : float 

365 3 times the standard error of the batch estimates. 

366 n_samples : int 

367 The number of samples actually used. 

368 """ 

369 sn = max(1.0, np.sqrt(nu)) 

370 low = np.asarray(low, dtype=np.float64) 

371 high = np.asarray(high, dtype=np.float64) 

372 cho, lo, hi = _permuted_cholesky(covar, low / sn, high / sn) 

373 n = cho.shape[0] 

374 prob = 0.0 

375 error_var = 0.0 

376 q, n_qmc_samples = _cbc_lattice(n, max(m // n_batches, 1)) 

377 i_samples = np.arange(n_qmc_samples) + 1 

378 for j in range(n_batches): 

379 pv = np.ones(n_qmc_samples) 

380 s = np.zeros((n, n_qmc_samples)) 

381 for i in range(n): 

382 # Pseudorandomly-shifted lattice coordinate. 

383 z = q[i] * i_samples + rng.random() 

384 # Fast remainder(z, 1.0) 

385 z -= z.astype(int) 

386 # Tent periodization transform. 

387 x = abs(2 * z - 1) 

388 # FIXME: Lift the i==0 case out of the loop to make the logic 

389 # easier to follow. 

390 if i == 0: 

391 # We'll use one of the QR variates to pull out the 

392 # t-distribution scaling. 

393 if nu > 0: 

394 r = np.sqrt(2 * gammaincinv(nu / 2, x)) 

395 else: 

396 r = np.ones_like(x) 

397 else: 

398 y = phinv(c + x * dc) # noqa: F821 

399 with np.errstate(invalid='ignore'): 

400 s[i:, :] += cho[i:, i - 1][:, np.newaxis] * y 

401 si = s[i, :] 

402 

403 c = np.ones(n_qmc_samples) 

404 d = np.ones(n_qmc_samples) 

405 with np.errstate(invalid='ignore'): 

406 lois = lo[i] * r - si 

407 hiis = hi[i] * r - si 

408 c[lois < -9] = 0.0 

409 d[hiis < -9] = 0.0 

410 lo_mask = abs(lois) < 9 

411 hi_mask = abs(hiis) < 9 

412 c[lo_mask] = phi(lois[lo_mask]) 

413 d[hi_mask] = phi(hiis[hi_mask]) 

414 

415 dc = d - c 

416 pv *= dc 

417 

418 # Accumulate the mean and error variances with online formulations. 

419 d = (pv.mean() - prob) / (j + 1) 

420 prob += d 

421 error_var = (j - 1) * error_var / (j + 1) + d * d 

422 # Error bounds are 3 times the standard error of the estimates. 

423 est_error = 3 * np.sqrt(error_var) 

424 n_samples = n_qmc_samples * n_batches 

425 return prob, est_error, n_samples 

426 

427 

428def _permuted_cholesky(covar, low, high, tol=1e-10): 

429 """Compute a scaled, permuted Cholesky factor, with integration bounds. 

430 

431 The scaling and permuting of the dimensions accomplishes part of the 

432 transformation of the original integration problem into a more numerically 

433 tractable form. The lower-triangular Cholesky factor will then be used in 

434 the subsequent integration. The integration bounds will be scaled and 

435 permuted as well. 

436 

437 Parameters 

438 ---------- 

439 covar : (n, n) float array 

440 Possibly singular, positive semidefinite symmetric covariance matrix. 

441 low, high : (n,) float array 

442 The low and high integration bounds. 

443 tol : float, optional 

444 The singularity tolerance. 

445 

446 Returns 

447 ------- 

448 cho : (n, n) float array 

449 Lower Cholesky factor, scaled and permuted. 

450 new_low, new_high : (n,) float array 

451 The scaled and permuted low and high integration bounds. 

452 """ 

453 # Make copies for outputting. 

454 cho = np.array(covar, dtype=np.float64) 

455 new_lo = np.array(low, dtype=np.float64) 

456 new_hi = np.array(high, dtype=np.float64) 

457 n = cho.shape[0] 

458 if cho.shape != (n, n): 

459 raise ValueError("expected a square symmetric array") 

460 if new_lo.shape != (n,) or new_hi.shape != (n,): 

461 raise ValueError( 

462 "expected integration boundaries the same dimensions " 

463 "as the covariance matrix" 

464 ) 

465 # Scale by the sqrt of the diagonal. 

466 dc = np.sqrt(np.maximum(np.diag(cho), 0.0)) 

467 # But don't divide by 0. 

468 dc[dc == 0.0] = 1.0 

469 new_lo /= dc 

470 new_hi /= dc 

471 cho /= dc 

472 cho /= dc[:, np.newaxis] 

473 

474 y = np.zeros(n) 

475 sqtp = np.sqrt(2 * np.pi) 

476 for k in range(n): 

477 epk = (k + 1) * tol 

478 im = k 

479 ck = 0.0 

480 dem = 1.0 

481 s = 0.0 

482 lo_m = 0.0 

483 hi_m = 0.0 

484 for i in range(k, n): 

485 if cho[i, i] > tol: 

486 ci = np.sqrt(cho[i, i]) 

487 if i > 0: 

488 s = cho[i, :k] @ y[:k] 

489 lo_i = (new_lo[i] - s) / ci 

490 hi_i = (new_hi[i] - s) / ci 

491 de = phi(hi_i) - phi(lo_i) 

492 if de <= dem: 

493 ck = ci 

494 dem = de 

495 lo_m = lo_i 

496 hi_m = hi_i 

497 im = i 

498 if im > k: 

499 # Swap im and k 

500 cho[im, im] = cho[k, k] 

501 _swap_slices(cho, np.s_[im, :k], np.s_[k, :k]) 

502 _swap_slices(cho, np.s_[im + 1:, im], np.s_[im + 1:, k]) 

503 _swap_slices(cho, np.s_[k + 1:im, k], np.s_[im, k + 1:im]) 

504 _swap_slices(new_lo, k, im) 

505 _swap_slices(new_hi, k, im) 

506 if ck > epk: 

507 cho[k, k] = ck 

508 cho[k, k + 1:] = 0.0 

509 for i in range(k + 1, n): 

510 cho[i, k] /= ck 

511 cho[i, k + 1:i + 1] -= cho[i, k] * cho[k + 1:i + 1, k] 

512 if abs(dem) > tol: 

513 y[k] = ((np.exp(-lo_m * lo_m / 2) - np.exp(-hi_m * hi_m / 2)) / 

514 (sqtp * dem)) 

515 else: 

516 y[k] = (lo_m + hi_m) / 2 

517 if lo_m < -10: 

518 y[k] = hi_m 

519 elif hi_m > 10: 

520 y[k] = lo_m 

521 cho[k, :k + 1] /= ck 

522 new_lo[k] /= ck 

523 new_hi[k] /= ck 

524 else: 

525 cho[k:, k] = 0.0 

526 y[k] = (new_lo[k] + new_hi[k]) / 2 

527 return cho, new_lo, new_hi 

528 

529 

530def _swap_slices(x, slc1, slc2): 

531 t = x[slc1].copy() 

532 x[slc1] = x[slc2].copy() 

533 x[slc2] = t