diff --git a/test/prior_tests.py b/test/prior_tests.py index cc105c4d81618e6f6253d245fa09b309cba36e19..566dc3f2897a2a0adb4b3c89538eca8aedab2cb1 100644 --- a/test/prior_tests.py +++ b/test/prior_tests.py @@ -111,5 +111,75 @@ class TestFixMethod(unittest.TestCase): self.assertRaises(ValueError, tupak.prior.fix, self.prior, np.nan) +class TestPriorClasses(unittest.TestCase): + + def setUp(self): + + self.priors = [ + tupak.prior.DeltaFunction(name='test', peak=1), + tupak.prior.Gaussian(name='test', mu=0, sigma=1), + tupak.prior.PowerLaw(name='test', alpha=0, minimum=0, maximum=1), + tupak.prior.PowerLaw(name='test', alpha=-1, minimum=1, maximum=1e2), + tupak.prior.Uniform(name='test', minimum=0, maximum=1), + tupak.prior.UniformComovingVolume(name='test', minimum=2e2, maximum=5e3), + tupak.prior.Sine(name='test'), + tupak.prior.Cosine(name='test'), + tupak.prior.Interped(name='test', xx=np.linspace(0, 10, 1000), yy=np.linspace(0, 10, 1000)**4, + minimum=3, maximum=5), + tupak.prior.TruncatedGaussian(name='test', mu=1, sigma=0.4, minimum=-1, maximum=1) + ] + + def test_rescaling(self): + for prior in self.priors: + """Test the the rescaling works as expected.""" + minimum_sample = prior.rescale(0) + self.assertAlmostEqual(minimum_sample, prior.minimum) + maximum_sample = prior.rescale(1) + self.assertAlmostEqual(maximum_sample, prior.maximum) + many_samples = prior.rescale(np.random.uniform(0, 1, 1000)) + self.assertTrue(all((many_samples >= prior.minimum) & (many_samples <= prior.maximum))) + self.assertRaises(ValueError, lambda: prior.rescale(-1)) + + def test_sampling(self): + """Test that sampling from the prior always returns values within its domain.""" + for prior in self.priors: + single_sample = prior.sample() + self.assertTrue((single_sample >= prior.minimum) & (single_sample <= prior.maximum)) + many_samples = prior.sample(1000) + self.assertTrue(all((many_samples >= prior.minimum) & (many_samples <= prior.maximum))) + + def test_prob(self): + """Test that the prior probability is non-negative in domain of validity and zero outside.""" + for prior in self.priors: + # skip delta function prior in this case + if isinstance(prior, tupak.prior.DeltaFunction): + continue + if prior.maximum != np.inf: + outside_domain = np.linspace(prior.maximum + 1, prior.maximum + 1e4, 1000) + self.assertTrue(all(prior.prob(outside_domain) == 0)) + if prior.minimum != -np.inf: + outside_domain = np.linspace(prior.minimum - 1e4, prior.minimum - 1, 1000) + self.assertTrue(all(prior.prob(outside_domain) == 0)) + if prior.minimum == -np.inf: + prior.minimum = -1e5 + if prior.maximum == np.inf: + prior.maximum = 1e5 + domain = np.linspace(prior.minimum, prior.maximum, 1000) + self.assertTrue(all(prior.prob(domain) >= 0)) + surround_domain = np.linspace(prior.minimum - 1, prior.maximum + 1, 1000) + prior.prob(surround_domain) + + def test_normalized(self): + """Test that each of the priors are normalised, this needs care for delta function and Gaussian priors""" + for prior in self.priors: + if isinstance(prior, tupak.prior.DeltaFunction): + continue + elif isinstance(prior, tupak.prior.Gaussian): + domain = np.linspace(-1e2, 1e2, 1000) + else: + domain = np.linspace(prior.minimum, prior.maximum, 1000) + self.assertAlmostEqual(np.trapz(prior.prob(domain), domain), 1, 3) + + if __name__ == '__main__': unittest.main() diff --git a/tupak/prior.py b/tupak/prior.py index 8eb15c937131cfdb5d69ed1d47ebf6ac12097169..bdce5908b9217ee95778bcd15636e359bb56286f 100644 --- a/tupak/prior.py +++ b/tupak/prior.py @@ -132,10 +132,8 @@ class Uniform(Prior): def prob(self, val): """Return the prior probability of val""" - if (self.minimum < val) & (val < self.maximum): - return 1 / self.support - else: - return 0 + in_prior = (val >= self.minimum) & (val <= self.maximum) + return 1 / self.support * in_prior class DeltaFunction(Prior): @@ -181,14 +179,12 @@ class PowerLaw(Prior): def prob(self, val): """Return the prior probability of val""" - if (val > self.minimum) and (val < self.maximum): - if self.alpha == -1: - return 1 / val / np.log(self.maximum / self.minimum) - else: - return val ** self.alpha * (1 + self.alpha) / (self.maximum ** (1 + self.alpha) - - self.minimum ** (1 + self.alpha)) + in_prior = (val >= self.minimum) & (val <= self.maximum) + if self.alpha == -1: + return np.nan_to_num(1 / val / np.log(self.maximum / self.minimum)) * in_prior else: - return 0 + return np.nan_to_num(val ** self.alpha * (1 + self.alpha) / (self.maximum ** (1 + self.alpha) + - self.minimum ** (1 + self.alpha))) * in_prior class Cosine(Prior): @@ -205,13 +201,10 @@ class Cosine(Prior): Prior.test_valid_for_rescaling(val) return np.arcsin(-1 + val * 2) - @staticmethod - def prob(val): + def prob(self, val): """Return the prior probability of val, defined over [-pi/2, pi/2]""" - if (val > np.minimum) and (val < np.maximum): - return np.cos(val) / 2 - else: - return 0 + in_prior = (val >= self.minimum) & (val <= self.maximum) + return np.cos(val) / 2 * in_prior class Sine(Prior): @@ -226,15 +219,12 @@ class Sine(Prior): This maps to the inverse CDF. This has been analytically solved for this case. """ Prior.test_valid_for_rescaling(val) - return np.arccos(-1 + val * 2) + return np.arccos(1 - val * 2) - @staticmethod - def prob(val): + def prob(self, val): """Return the prior probability of val, defined over [0, pi]""" - if (val > 0) and (val < np.pi): - return np.sin(val) / 2 - else: - return 0 + in_prior = (val >= self.minimum) & (val <= self.maximum) + return np.sin(val) / 2 * in_prior class Gaussian(Prior): @@ -290,11 +280,9 @@ class TruncatedGaussian(Prior): def prob(self, val): """Return the prior probability of val""" - if (val > self.minimum) & (val < self.maximum): - return np.exp(-(self.mu - val) ** 2 / (2 * self.sigma ** 2)) / ( - 2 * np.pi) ** 0.5 / self.sigma / self.normalisation - else: - return 0 + in_prior = (val >= self.minimum) & (val <= self.maximum) + return np.exp(-(self.mu - val) ** 2 / (2 * self.sigma ** 2)) / ( + 2 * np.pi) ** 0.5 / self.sigma / self.normalisation * in_prior class Interped(Prior): @@ -302,6 +290,7 @@ class Interped(Prior): def __init__(self, xx, yy, minimum=None, maximum=None, name=None, latex_label=None): """Initialise object from arrays of x and y=p(x)""" Prior.__init__(self, name, latex_label) + all_interpolated = interp1d(x=xx, y=yy, bounds_error=False, fill_value=0) if minimum is None or minimum < min(xx): self.minimum = min(xx) else: @@ -310,22 +299,21 @@ class Interped(Prior): self.maximum = max(xx) else: self.maximum = maximum - self.xx = xx[(xx > self.minimum) & (xx < self.maximum)] - self.yy = yy[(xx > self.minimum) & (xx < self.maximum)] + self.xx = np.linspace(self.minimum, self.maximum, len(xx)) + self.yy = all_interpolated(self.xx) if np.trapz(self.yy, self.xx) != 1: logging.info('Supplied PDF is not normalised, normalising.') self.yy /= np.trapz(self.yy, self.xx) self.YY = cumtrapz(self.yy, self.xx, initial=0) + # Need last element of cumulative distribution to be exactly one. + self.YY[-1] = 1 self.probability_density = interp1d(x=self.xx, y=self.yy, bounds_error=False, fill_value=0) self.cumulative_distribution = interp1d(x=self.xx, y=self.YY, bounds_error=False, fill_value=0) self.inverse_cumulative_distribution = interp1d(x=self.YY, y=self.xx, bounds_error=True) def prob(self, val): """Return the prior probability of val""" - if (val > self.minimum) & (val < self.maximum): - return self.probability_density(val) - else: - return 0 + return self.probability_density(val) def rescale(self, val): """