def test_basic_all_scipy_dists(self, distname, shapes): slow_dists = {'ksone', 'kstwo', 'levy_stable', 'skewnorm'} fail_dists = { 'beta', 'gausshyper', 'geninvgauss', 'ncf', 'nct', 'norminvgauss', 'genhyperbolic', 'studentized_range', 'vonmises', 'kappa4', 'invgauss', 'wald' } if distname in slow_dists: pytest.skip("Distribution is too slow") if distname in fail_dists: # specific reasons documented in gh-13319 # https://github.com/scipy/scipy/pull/13319#discussion_r626188955 pytest.xfail("Fails - usually due to inaccurate CDF/PDF") np.random.seed(0) dist = getattr(stats, distname)(*shapes) with np.testing.suppress_warnings() as sup: sup.filter(RuntimeWarning, "overflow encountered") sup.filter(RuntimeWarning, "divide by zero") sup.filter(RuntimeWarning, "invalid value encountered") fni = NumericalInverseHermite(dist) x = np.random.rand(10) p_tol = np.max(np.abs(dist.ppf(x) - fni.ppf(x)) / np.abs(dist.ppf(x))) u_tol = np.max(np.abs(dist.cdf(fni.ppf(x)) - x)) assert p_tol < 1e-8 assert u_tol < 1e-12
def test_QRVS_size_tuple(self): # QMCEngine samples are always of shape (n, d). When `size` is a tuple, # we set `n = prod(size)` in the call to qmc_engine.random, transform # the sample, and reshape it to the final dimensions. When we reshape, # we need to be careful, because the _columns_ of the sample returned # by a QMCEngine are "independent"-ish, but the elements within the # columns are not. We need to make sure that this doesn't get mixed up # by reshaping: qrvs[..., i] should remain "independent"-ish of # qrvs[..., i+1], but the elements within qrvs[..., i] should be # transformed from the same low-discrepancy sequence. if NumpyVersion(np.__version__) <= '1.18.0': pytest.skip("QMC doesn't play well with old NumPy") dist = StandardNormal() fni = NumericalInverseHermite(dist) size = (3, 4) d = 5 qrng = stats.qmc.Halton(d, seed=0) qrng2 = stats.qmc.Halton(d, seed=0) uniform = qrng2.random(np.prod(size)) qrvs = fni.qrvs(size=size, d=d, qmc_engine=qrng) qrvs2 = stats.norm.ppf(uniform) for i in range(d): sample = qrvs[..., i] sample2 = qrvs2[:, i].reshape(size) assert_allclose(sample, sample2, atol=1e-12)
def test_QRVS(self, qrng, size_in, size_out, d_in, d_out): dist = StandardNormal() fni = NumericalInverseHermite(dist) # If d and qrng.d are inconsistent, an error is raised if d_in is not None and qrng is not None and qrng.d != d_in: match = "`d` must be consistent with dimension of `qmc_engine`." with pytest.raises(ValueError, match=match): fni.qrvs(size_in, d=d_in, qmc_engine=qrng) return # Sometimes d is really determined by qrng if d_in is None and qrng is not None and qrng.d != 1: d_out = (qrng.d, ) shape_expected = size_out + d_out qrng2 = deepcopy(qrng) qrvs = fni.qrvs(size=size_in, d=d_in, qmc_engine=qrng) if size_in is not None: assert (qrvs.shape == shape_expected) if qrng2 is not None: uniform = qrng2.random(np.prod(size_in) or 1) qrvs2 = stats.norm.ppf(uniform).reshape(shape_expected) assert_allclose(qrvs, qrvs2, atol=1e-12)
def test_input_validation(self): match = r"`order` must be either 1, 3, or 5." with pytest.raises(ValueError, match=match): NumericalInverseHermite(StandardNormal(), order=2) match = "`cdf` required but not found" with pytest.raises(ValueError, match=match): NumericalInverseHermite("norm") match = "could not convert string to float" with pytest.raises(ValueError, match=match): NumericalInverseHermite(StandardNormal(), u_resolution='ekki')
def test_inaccurate_CDF(self): # CDF function with inaccurate tail cannot be inverted; see gh-13319 # https://github.com/scipy/scipy/pull/13319#discussion_r626188955 shapes = (2.3098496451481823, 0.6268795430096368) match = ("98 : one or more intervals very short; possibly due to " "numerical problems with a pole or very flat tail") # fails with default tol with pytest.warns(RuntimeWarning, match=match): NumericalInverseHermite(stats.beta(*shapes)) # no error with coarser tol NumericalInverseHermite(stats.beta(*shapes), u_resolution=1e-8)
def test_RVS(self, rng, size_in, size_out): dist = StandardNormal() fni = NumericalInverseHermite(dist) rng2 = deepcopy(rng) rvs = fni.rvs(size=size_in, random_state=rng) if size_in is not None: assert (rvs.shape == size_out) if rng2 is not None: rng2 = check_random_state(rng2) uniform = rng2.uniform(size=size_in) rvs2 = stats.norm.ppf(uniform) assert_allclose(rvs, rvs2)
def test_ppf(self, u): dist = StandardNormal() rng = NumericalInverseHermite(dist, u_resolution=1e-12) # Older versions of NumPy throw RuntimeWarnings for comparisons # with nan. with suppress_warnings() as sup: sup.filter(RuntimeWarning, "invalid value encountered in greater") sup.filter(RuntimeWarning, "invalid value encountered in " "greater_equal") sup.filter(RuntimeWarning, "invalid value encountered in less") sup.filter(RuntimeWarning, "invalid value encountered in " "less_equal") res = rng.ppf(u) expected = stats.norm.ppf(u) assert_allclose(res, expected, rtol=1e-9, atol=3e-10) assert res.shape == expected.shape
def test_custom_distribution(self): dist1 = StandardNormal() fni1 = NumericalInverseHermite(dist1) dist2 = stats.norm() fni2 = NumericalInverseHermite(dist2) assert_allclose(fni1.rvs(random_state=0), fni2.rvs(random_state=0))
def test_u_error(self): dist = StandardNormal() rng = NumericalInverseHermite(dist, u_resolution=1e-10) max_error, mae = rng.u_error() assert max_error < 1e-10 assert mae <= max_error with suppress_warnings() as sup: # ignore warning about u-resolution being too small. sup.filter(RuntimeWarning) rng = NumericalInverseHermite(dist, u_resolution=1e-14) max_error, mae = rng.u_error() assert max_error < 1e-14 assert mae <= max_error
def test_with_scipy_distribution(): # test if the setup works with SciPy's rv_frozen distributions dist = stats.norm() urng = np.random.default_rng(0) rng = NumericalInverseHermite(dist, random_state=urng) u = np.linspace(0, 1, num=100) check_cont_samples(rng, dist, dist.stats()) assert_allclose(dist.ppf(u), rng.ppf(u)) # test if it works with `loc` and `scale` dist = stats.norm(loc=10., scale=5.) rng = NumericalInverseHermite(dist, random_state=urng) check_cont_samples(rng, dist, dist.stats()) assert_allclose(dist.ppf(u), rng.ppf(u)) # check for discrete distributions dist = stats.binom(10, 0.2) rng = DiscreteAliasUrn(dist, random_state=urng) domain = dist.support() pv = dist.pmf(np.arange(domain[0], domain[1] + 1)) check_discr_samples(rng, pv, dist.stats())
def test_inf_nan_domains(self, domain, err, msg): with pytest.raises(err, match=msg): NumericalInverseHermite(StandardNormal(), domain=domain)
def test_basic(self, dist, mv_ex, order): rng = NumericalInverseHermite(dist, order=order, random_state=42) check_cont_samples(rng, dist, mv_ex)
def test_input_validation(self): match = r"`order` must be either 1, 3, or 5." with pytest.raises(ValueError, match=match): NumericalInverseHermite(StandardNormal(), order=2) match = "`cdf` required but not found" with pytest.raises(ValueError, match=match): NumericalInverseHermite("norm") match = "could not convert string to float" with pytest.raises(ValueError, match=match): NumericalInverseHermite(StandardNormal(), u_resolution='ekki') match = "`max_intervals' must be..." with pytest.raises(ValueError, match=match): NumericalInverseHermite(StandardNormal(), max_intervals=-1) match = "`qmc_engine` must be an instance of..." with pytest.raises(ValueError, match=match): fni = NumericalInverseHermite(StandardNormal()) fni.qrvs(qmc_engine=0) if NumpyVersion(np.__version__) >= '1.18.0': # issues with QMCEngines and old NumPy fni = NumericalInverseHermite(StandardNormal()) match = "`d` must be consistent with dimension of `qmc_engine`." with pytest.raises(ValueError, match=match): fni.qrvs(d=3, qmc_engine=stats.qmc.Halton(2))
def test_deprecations(self): msg = ("`tol` has been deprecated and replaced with `u_resolution`. " "It will be completely removed in a future release.") with pytest.warns(DeprecationWarning, match=msg): NumericalInverseHermite(StandardNormal(), tol=1e-12)