diff --git a/tupak/core/likelihood.py b/tupak/core/likelihood.py index c6048b40d38622a9f38ce62d5177201ca297cc1c..5784c859b53feee1ae811cea259aa0e634572187 100644 --- a/tupak/core/likelihood.py +++ b/tupak/core/likelihood.py @@ -137,7 +137,10 @@ class GaussianLikelihood(Likelihood): model = self.function(self.x, **sampler.pymc3_priors) - return sampler.external_sampler.Normal('likelihood', mu=model, sd=self.sigma, observed=self.y) + # set the distribution + sampler.external_sampler.Normal('likelihood', mu=model, + sd=self.sigma, observed=self.y) + class PoissonLikelihood(Likelihood): def __init__(self, x, counts, func): @@ -258,5 +261,6 @@ class PoissonLikelihood(Likelihood): # get rate function rate = self.function(self.x, **sampler.pymc3_priors) - return sampler.external_sampler.Poisson('likelihood', mu=rate, - observed=self.y) + # set the distribution + sampler.external_sampler.Poisson('likelihood', mu=rate, + observed=self.y) diff --git a/tupak/core/prior.py b/tupak/core/prior.py index d48617970b81683ca4f6fcc8001e47d91be63706..0715476f1be98c69868be17f26595d91467a39ba 100644 --- a/tupak/core/prior.py +++ b/tupak/core/prior.py @@ -541,10 +541,6 @@ class DeltaFunction(Prior): else: return 0 - def pymc3_prior(self, sampler): - # just return the value - return self.peak - def __repr__(self): """Call to helper method in the super class.""" return Prior._subclass_repr_helper(self, subclass_args=['peak']) @@ -651,10 +647,6 @@ class Uniform(Prior): """ Prior.__init__(self, name, latex_label, minimum, maximum) - # set PyMC3 Uniform distribution attributes - self.lower = self.minimum - self.upper = self.maximum - def rescale(self, val): Prior.test_valid_for_rescaling(val) return self.minimum + val * (self.maximum - self.minimum) @@ -860,17 +852,11 @@ class Gaussian(Prior): class Normal(Gaussian): def __init__(self, mu, sigma, name=None, latex_label=None): - """A copy of the Gaussian prior, but with "Normal" name to copy that - used for the distribution in PyMC3. - + """A synonym for the Gaussian prior """ Gaussian.__init__(self, mu, sigma, name, latex_label) - # set argument names used in PyMC3 distribution - self.mu = mu - self.sd = sigma - class TruncatedGaussian(Prior): @@ -939,6 +925,14 @@ class TruncatedGaussian(Prior): return Prior._subclass_repr_helper(self, subclass_args=['mu', 'sigma']) +class TruncatedNormal(Gaussian): + def __init__(self, mu, sigma, minimum, maximum, name=None, latex_label=None): + """A synonym for the Truncated Gaussian prior + """ + + TruncatedGaussian.__init__(self, mu, sigma, minimum, maximum, name, latex_label) + + class Interped(Prior): def __init__(self, xx, yy, minimum=np.nan, maximum=np.nan, name=None, latex_label=None): diff --git a/tupak/core/sampler.py b/tupak/core/sampler.py index b3cee6884c4acfd5f010de928d323883c3b8263d..35eea0c933b45673987acd57b091235603ddc404 100644 --- a/tupak/core/sampler.py +++ b/tupak/core/sampler.py @@ -11,7 +11,7 @@ from collections import OrderedDict from tupak.core.utils import logger from tupak.core.result import Result, read_in_result -from tupak.core.prior import Prior, DeltaFunction +from tupak.core.prior import Prior from tupak.core import utils import tupak @@ -944,6 +944,144 @@ class Ptemcee(Emcee): class Pymc3(Sampler): """ https://docs.pymc.io/ """ + def setup_prior_mapping(self): + """ + Set the mapping between predefined tupak priors and the equivalent + PyMC3 distributions. + """ + + self.prior_map = {} + + # predefined PyMC3 distributions + + # Gaussian + self.prior_map['Gaussian'] = {'pymc3': 'Normal', + 'argmap': {'mu': 'mu', 'sigma': 'sd'}} + self.prior_map['Normal'] = self.prior_map['Gaussian'] + + # Truncated Gaussian + self.prior_map['TruncatedGaussian'] = {'pymc3': 'TruncatedNormal', + 'argmap': {'mu': 'mu', 'sigma': 'sd', 'minimum': 'lower', 'maximum': 'upper'}} + self.prior_map['TruncatedNormal'] = self.prior_map['TruncatedGaussian'] + + # Uniform + self.prior_map['Uniform'] = {'pymc3': 'Uniform', + 'argmap': {'minimum': 'lower', 'maximum': 'upper'}} + + # Interpolated + self.prior_map['Interped'] = {'pymc3': 'Interpolated', + 'argmap': {'xx': 'x_points', 'yy': 'pdf_points'}} + + # internally defined mappings for tupak priors + + # DeltaFunction + self.prior_map['DeltaFunction'] = {'internal': self._deltafunction_prior} + + # Sine + self.prior_map['Sine'] = {'internal': self._sine_prior} + + # Cosine + self.prior_map['Cosine'] = {'internal': self._cosine_prior} + + def _deltafunction_prior(self, key): + """ + Map the tupak delta function prior to a single value for PyMC3. + """ + + from tupak.core.prior import DeltaFunction + + # check prior is a DeltaFunction + if isinstance(self.priors[key], DeltaFunction): + return self.priors[key].peak + else: + raise ValueError("Prior for '{}' is not a DeltaFunction".format(key)) + + def _sine_prior(self, key): + """ + Map the tupak Sine prior to a PyMC3 style function + """ + + from tupak.core.prior import Sine + + # check prior is a Sine + if isinstance(self.priors[key], Sine): + pymc3 = self.external_sampler + + # import theano + try: + import theano.tensor as tt + from pymc3.theanof import floatX + except ImportError: + raise ImportError("You must have Theano installed to use PyMC3") + + class Pymc3Sine(pymc3.Continuous): + def __init__(self, lower=0., upper=np.pi): + if lower >= upper: + raise ValueError("Lower bound is above upper bound!") + + # set the mode + self.lower = lower = tt.as_tensor_variable(floatX(lower)) + self.upper = upper = tt.as_tensor_variable(floatX(upper)) + self.norm = (tt.cos(lower) - tt.cos(upper)) + self.mean = (tt.sin(upper)+lower*tt.cos(lower) - tt.sin(lower) - upper*tt.cos(upper))/self.norm + + transform = pymc3.distributions.transforms.interval(lower, upper) + + super(Pymc3Sine, self).__init__(transform=transform) + + def logp(self, value): + upper = self.upper + lower = self.lower + return pymc3.distributions.dist_math.bound(tt.log(tt.sin(value)/self.norm), lower <= value, value <= upper) + + return Pymc3Sine(key, lower=self.priors[key].minimum, upper=self.priors[key].maximum) + else: + raise ValueError("Prior for '{}' is not a Sine".format(key)) + + def _cosine_prior(self, key): + """ + Map the tupak Cosine prior to a PyMC3 style function + """ + + from tupak.core.prior import Cosine + + # check prior is a Cosine + if isinstance(self.priors[key], Cosine): + pymc3 = self.external_sampler + + # import theano + try: + import theano.tensor as tt + from pymc3.theanof import floatX + except ImportError: + raise ImportError("You must have Theano installed to use PyMC3") + + class Pymc3Cosine(pymc3.Continuous): + def __init__(self, lower=-np.pi/2., upper=np.pi/2.): + if lower >= upper: + raise ValueError("Lower bound is above upper bound!") + + self.norm = (np.sin(upper) - np.sin(lower)) + self.mean = (upper*np.sin(upper)+np.cos(upper)-lower*np.sin(lower)-np.cos(lower))/self.norm + + self.lower = lower = tt.as_tensor_variable(floatX(lower)) + self.upper = upper = tt.as_tensor_variable(floatX(upper)) + self.norm = (tt.sin(upper) - tt.sin(lower)) + self.mean = (upper*tt.sin(upper) + tt.cos(upper)-lower*tt.sin(lower)-tt.cos(lower))/self.norm + + transform = pymc3.distributions.transforms.interval(lower, upper) + + super(Pymc3Cosine, self).__init__(transform=transform) + + def logp(self, value): + upper = self.upper + lower = self.lower + return pymc3.distributions.dist_math.bound(tt.log(tt.cos(value)/self.norm), lower <= value, value <= upper) + + return Pymc3Cosine(key, lower=self.priors[key].minimum, upper=self.priors[key].maximum) + else: + raise ValueError("Prior for '{}' is not a Sine".format(key)) + def _run_external_sampler(self): pymc3 = self.external_sampler @@ -954,20 +1092,21 @@ class Pymc3(Sampler): self.tune = self.kwargs.get('tune', 1000) # burn in samples self.discard_tuned_samples = self.kwargs.get('discard_tuned_samples', True) + # set the model model = pymc3.Model() with model: self.set_prior() - likelihood = self.likelihood.pymc3_likelihood(self) + self.likelihood.pymc3_likelihood(self) # perform the sampling trace = pymc3.sample(self.draws, tune=self.tune, cores=self.cores, chains=self.chains, discard_tuned_samples=self.discard_tuned_samples) - nparams = len(trace.varnames) + nparams = len(self.priors.keys()) nsamples = len(trace)*self.chains self.result.samples = np.zeros((nsamples, nparams)) @@ -985,8 +1124,12 @@ class Pymc3(Sampler): """ + self.setup_prior_mapping() + self.pymc3_priors = dict() + pymc3 = self.external_sampler + # set the parameter prior distributions for key in self.priors: # if the prior contains a pymc3_prior method use that otherwise try @@ -997,24 +1140,32 @@ class Pymc3(Sampler): # use Prior distribution name distname = self.priors[key].__class__.__name__ - # check whether name is a PyMC3 distribution - if distname in self.external_sampler.__dict__: - # check the required arguments for the PyMC3 distribution - reqargs = inspect.getargspec(self.external_sampler.__dict__[distname].__init__).args[1:] - - priorkwargs = dict() - - # check whether the Prior class has required attributes - for arg in reqargs: - if hasattr(self.priors[key], arg): - priorkwargs[arg] = getattr(self.priors[key], arg) - else: - priorkwargs[arg] = None - - # set the prior - self.pymc3_priors[key] = self.external_sampler.__dict__[distname](key, **priorkwargs) + if distname in self.prior_map: + # check if we have a predefined PyMC3 distribution + if 'pymc3' in self.prior_map[distname] and 'argmap' in self.prior_map[distname] and distname in pymc3.__dict__: + # check the required arguments for the PyMC3 distribution + pymc3distname = self.prior_map[distname]['pymc3'] + + reqargs = inspect.getargspec(pymc3.__dict__[pymc3distname].__init__).args[1:] + + # set keyword arguments + priorkwargs = {} + for (targ, parg) in self.prior_map[distname]['argmap'].items(): + if hasattr(self.priors[key], targ): + if parg in reqargs: + priorkwargs[parg] = getattr(self.priors[key], targ) + else: + raise ValueError("Unknown argument {}".format(parg)) + else: + if parg in reqargs: + priorkwargs[parg] = None + self.pymc3_priors[key] = pymc3.__dict__[pymc3distname](key, **priorkwargs) + elif 'internal' in self.prior_map[distname]: + self.pymc3_priors[key] = self.prior_map[distname]['internal'](key) + else: + raise ValueError("Prior '{}' is not a known distribution.".format(distname)) else: - raise ValueError("Prior '{}' is not a PyMC3 distribution.".format(distname)) + raise ValueError("Prior '{}' is not a known distribution.".format(distname)) def calculate_autocorrelation(self, samples, c=3): """ Uses the `emcee.autocorr` module to estimate the autocorrelation