Source code for pycbc.distributions.angular

# Copyright (C) 2016  Collin Capano
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""
This modules provides classes for evaluating angular distributions.
"""
import logging
from configparser import Error
import numpy

from pycbc import VARARGS_DELIM
from pycbc import boundaries
from pycbc.distributions import bounded
from pycbc.distributions import uniform

logger = logging.getLogger('pycbc.distributions.angular')


[docs]class UniformAngle(uniform.Uniform): """A uniform distribution in which the dependent variable is between `[0,2pi)`. The domain of the distribution may optionally be made cyclic using the `cyclic_domain` parameter. Bounds may be provided to limit the range for which the pdf has support. If provided, the parameter bounds are in radians. Parameters ---------- cyclic_domain : {False, bool} If True, cyclic bounds on [0, 2pi) are applied to all values when evaluating the pdf. This is done prior to any additional bounds specified for a parameter are applied. Default is False. \**params : The keyword arguments should provide the names of parameters and (optionally) their corresponding bounds, as either `boundaries.Bounds` instances or tuples. The bounds must be in [0,2PI). These are converted to radians for storage. None may also be passed; in that case, the domain bounds will be used. Notes ------ For more information, see Uniform. """ name = 'uniform_angle' _domainbounds = (0, 2*numpy.pi) def __init__(self, cyclic_domain=False, **params): # _domain is a bounds instance used to apply cyclic conditions; this is # applied first, before any bounds specified in the initialization # are used self._domain = boundaries.Bounds(self._domainbounds[0], self._domainbounds[1], cyclic=cyclic_domain) for p,bnds in params.items(): if bnds is None: bnds = self._domain elif isinstance(bnds, boundaries.Bounds): # convert to radians bnds._min = bnds._min.__class__(bnds._min) bnds._max = bnds._max.__class__(bnds._max) else: # create a Bounds instance from the given tuple bnds = boundaries.Bounds(bnds[0], bnds[1]) # check that the bounds are in the domain if bnds.min < self._domain.min or bnds.max > self._domain.max: raise ValueError("bounds must be in [{x},{y}); " "got [{a},{b})".format(x=self._domain.min, y=self._domain.max, a=bnds.min, b=bnds.max)) # update params[p] = bnds super(UniformAngle, self).__init__(**params) @property def domain(self): """Returns the domain of the distribution.""" return self._domain
[docs] def apply_boundary_conditions(self, **kwargs): """Maps values to be in [0, 2pi) (the domain) first, before applying any additional boundary conditions. Parameters ---------- \**kwargs : The keyword args should be the name of a parameter and value to apply its boundary conditions to. The arguments need not include all of the parameters in self. Returns ------- dict A dictionary of the parameter names and the conditioned values. """ # map values to be within the domain kwargs = dict([[p, self._domain.apply_conditions(val)] for p,val in kwargs.items() if p in self._bounds]) # now apply additional conditions return super(UniformAngle, self).apply_boundary_conditions(**kwargs)
[docs] @classmethod def from_config(cls, cp, section, variable_args): """Returns a distribution based on a configuration file. The parameters for the distribution are retrieved from the section titled "[`section`-`variable_args`]" in the config file. By default, only the name of the distribution (`uniform_angle`) needs to be specified. This will results in a uniform prior on `[0, 2pi)`. To make the domain cyclic, add `cyclic_domain =`. To specify boundaries that are not `[0, 2pi)`, add `(min|max)-var` arguments, where `var` is the name of the variable. For example, this will initialize a variable called `theta` with a uniform distribution on `[0, 2pi)` without cyclic boundaries: .. code-block:: ini [{section}-theta] name = uniform_angle This will make the domain cyclic on `[0, 2pi)`: .. code-block:: ini [{section}-theta] name = uniform_angle cyclic_domain = Parameters ---------- cp : pycbc.workflow.WorkflowConfigParser A parsed configuration file that contains the distribution options. section : str Name of the section in the configuration file. variable_args : str The names of the parameters for this distribution, separated by ``VARARGS_DELIM``. These must appear in the "tag" part of the section header. Returns ------- UniformAngle A distribution instance from the pycbc.inference.prior module. """ # we'll retrieve the setting for cyclic_domain directly additional_opts = {'cyclic_domain': cp.has_option_tag(section, 'cyclic_domain', variable_args)} return bounded.bounded_from_config(cls, cp, section, variable_args, bounds_required=False, additional_opts=additional_opts)
[docs]class SinAngle(UniformAngle): r"""A sine distribution; the pdf of each parameter `\theta` is given by: ..math:: p(\theta) = \frac{\sin \theta}{\cos\theta_0 - \cos\theta_1}, \theta_0 \leq \theta < \theta_1, and 0 otherwise. Here, :math:`\theta_0, \theta_1` are the bounds of the parameter. The domain of this distribution is `[0, pi]`. This is accomplished by putting hard boundaries at `[0, pi]`. Bounds may be provided to further limit the range for which the pdf has support. As with `UniformAngle`, these are initialized in radians. Parameters ---------- \**params : The keyword arguments should provide the names of parameters and (optionally) their corresponding bounds, as either `boundaries.Bounds` instances or tuples. The bounds must be in [0,PI]. These are converted to radians for storage. None may also be passed; in that case, the domain bounds will be used. """ name = 'sin_angle' _func = numpy.cos _dfunc = numpy.sin _arcfunc = numpy.arccos _domainbounds = (0, numpy.pi) def __init__(self, **params): super(SinAngle, self).__init__(**params) # replace the domain self._domain = boundaries.Bounds(self._domainbounds[0], self._domainbounds[1], btype_min='closed', btype_max='closed', cyclic=False) self._lognorm = -sum([numpy.log( abs(self._func(bnd[1]) - self._func(bnd[0]))) \ for bnd in self._bounds.values()]) self._norm = numpy.exp(self._lognorm) def _cdfinv_param(self, arg, value): """Return inverse of cdf for mapping unit interval to parameter bounds. """ scale = (numpy.cos(self._bounds[arg][0]) - numpy.cos(self._bounds[arg][1])) offset = 1. + numpy.cos(self._bounds[arg][1]) / scale new_value = numpy.arccos(-scale * (value - offset)) return new_value def _pdf(self, **kwargs): """Returns the pdf at the given values. The keyword arguments must contain all of parameters in self's params. Unrecognized arguments are ignored. """ if kwargs not in self: return 0. return self._norm * \ self._dfunc(numpy.array([kwargs[p] for p in self._params])).prod() def _logpdf(self, **kwargs): """Returns the log of the pdf at the given values. The keyword arguments must contain all of parameters in self's params. Unrecognized arguments are ignored. """ if kwargs not in self: return -numpy.inf return self._lognorm + \ numpy.log(self._dfunc( numpy.array([kwargs[p] for p in self._params]))).sum()
[docs]class CosAngle(SinAngle): r"""A cosine distribution. This is the same thing as a sine distribution, but with the domain shifted to `[-pi/2, pi/2]`. See SinAngle for more details. Parameters ---------- \**params : The keyword arguments should provide the names of parameters and (optionally) their corresponding bounds, as either `boundaries.Bounds` instances or tuples. The bounds must be in [-PI/2, PI/2]. """ name = 'cos_angle' _func = numpy.sin _dfunc = numpy.cos _arcfunc = numpy.arcsin _domainbounds = (-numpy.pi/2, numpy.pi/2) def _cdfinv_param(self, param, value): a = self._bounds[param][0] b = self._bounds[param][1] scale = numpy.sin(b) - numpy.sin(a) offset = 1. - numpy.sin(b)/(numpy.sin(b) - numpy.sin(a)) new_value = numpy.arcsin((value - offset) * scale) return new_value
[docs]class UniformSolidAngle(bounded.BoundedDist): """A distribution that is uniform in the solid angle of a sphere. The names of the two angluar parameters can be specified on initalization. Parameters ---------- polar_angle : {'theta', str} The name of the polar angle. azimuthal_angle : {'phi', str} The name of the azimuthal angle. polar_bounds : {None, tuple} Limit the polar angle to the given bounds. If None provided, the polar angle will vary from 0 (the north pole) to pi (the south pole). The bounds should be specified as factors of pi. For example, to limit the distribution to the northern hemisphere, set `polar_bounds=(0,0.5)`. azimuthal_bounds : {None, tuple} Limit the azimuthal angle to the given bounds. If None provided, the azimuthal angle will vary from 0 to 2pi. The bounds should be specified as factors of pi. For example, to limit the distribution to the one hemisphere, set `azimuthal_bounds=(0,1)`. azimuthal_cyclic_domain : {False, bool} Make the domain of the azimuthal angle be cyclic; i.e., azimuthal values are constrained to be in [0, 2pi) using cyclic boundaries prior to applying any other boundary conditions and prior to evaluating the pdf. Default is False. """ name = 'uniform_solidangle' _polardistcls = SinAngle _azimuthaldistcls = UniformAngle _default_polar_angle = 'theta' _default_azimuthal_angle = 'phi' def __init__(self, polar_angle=None, azimuthal_angle=None, polar_bounds=None, azimuthal_bounds=None, azimuthal_cyclic_domain=False): if polar_angle is None: polar_angle = self._default_polar_angle if azimuthal_angle is None: azimuthal_angle = self._default_azimuthal_angle self._polardist = self._polardistcls(**{ polar_angle: polar_bounds}) self._azimuthaldist = self._azimuthaldistcls(**{ azimuthal_angle: azimuthal_bounds, 'cyclic_domain': azimuthal_cyclic_domain}) self._polar_angle = polar_angle self._azimuthal_angle = azimuthal_angle self._bounds = self._polardist.bounds.copy() self._bounds.update(self._azimuthaldist.bounds) self._params = sorted(self._bounds.keys()) @property def bounds(self): """dict: The bounds on each angle. The keys are the names of the polar and azimuthal angles, the values are the minimum and maximum of each, in radians. For example, if the distribution was initialized with `polar_angle='theta', polar_bounds=(0,0.5)` then the bounds will have `'theta': 0, 1.5707963267948966` as an entry.""" return self._bounds @property def polar_angle(self): """str: The name of the polar angle.""" return self._polar_angle @property def azimuthal_angle(self): """str: The name of the azimuthal angle.""" return self._azimuthal_angle def _cdfinv_param(self, param, value): """ Return the cdfinv for a single given parameter """ if param == self.polar_angle: return self._polardist._cdfinv_param(param, value) elif param == self.azimuthal_angle: return self._azimuthaldist._cdfinv_param(param, value)
[docs] def apply_boundary_conditions(self, **kwargs): """Maps the given values to be within the domain of the azimuthal and polar angles, before applying any other boundary conditions. Parameters ---------- \**kwargs : The keyword args must include values for both the azimuthal and polar angle, using the names they were initilialized with. For example, if `polar_angle='theta'` and `azimuthal_angle=`phi`, then the keyword args must be `theta={val1}, phi={val2}`. Returns ------- dict A dictionary of the parameter names and the conditioned values. """ polarval = kwargs[self._polar_angle] azval = kwargs[self._azimuthal_angle] # constrain each angle to its domain polarval = self._polardist._domain.apply_conditions(polarval) azval = self._azimuthaldist._domain.apply_conditions(azval) # apply any other boundary conditions polarval = self._bounds[self._polar_angle].apply_conditions(polarval) azval = self._bounds[self._azimuthal_angle].apply_conditions(azval) return {self._polar_angle: polarval, self._azimuthal_angle: azval}
def _pdf(self, **kwargs): """ Returns the pdf at the given angles. Parameters ---------- \**kwargs: The keyword arguments should specify the value for each angle, using the names of the polar and azimuthal angles as the keywords. Unrecognized arguments are ignored. Returns ------- float The value of the pdf at the given values. """ return self._polardist._pdf(**kwargs) * \ self._azimuthaldist._pdf(**kwargs) def _logpdf(self, **kwargs): """ Returns the logpdf at the given angles. Parameters ---------- \**kwargs: The keyword arguments should specify the value for each angle, using the names of the polar and azimuthal angles as the keywords. Unrecognized arguments are ignored. Returns ------- float The value of the pdf at the given values. """ return self._polardist._logpdf(**kwargs) +\ self._azimuthaldist._logpdf(**kwargs)
[docs] @classmethod def from_config(cls, cp, section, variable_args): """Returns a distribution based on a configuration file. The section must have the names of the polar and azimuthal angles in the tag part of the section header. For example: .. code-block:: ini [prior-theta+phi] name = uniform_solidangle If nothing else is provided, the default names and bounds of the polar and azimuthal angles will be used. To specify a different name for each angle, set the `polar-angle` and `azimuthal-angle` attributes. For example: .. code-block:: ini [prior-foo+bar] name = uniform_solidangle polar-angle = foo azimuthal-angle = bar Note that the names of the variable args in the tag part of the section name must match the names of the polar and azimuthal angles. Bounds may also be specified for each angle, as factors of pi. For example: .. code-block:: ini [prior-theta+phi] polar-angle = theta azimuthal-angle = phi min-theta = 0 max-theta = 0.5 This will return a distribution that is uniform in the upper hemisphere. By default, the domain of the azimuthal angle is `[0, 2pi)`. To make this domain cyclic, add `azimuthal_cyclic_domain =`. Parameters ---------- cp : ConfigParser instance The config file. section : str The name of the section. variable_args : str The names of the parameters for this distribution, separated by ``VARARGS_DELIM``. These must appear in the "tag" part of the section header. Returns ------- UniformSolidAngle A distribution instance from the pycbc.inference.prior module. """ tag = variable_args variable_args = variable_args.split(VARARGS_DELIM) # get the variables that correspond to the polar/azimuthal angles try: polar_angle = cp.get_opt_tag(section, 'polar-angle', tag) except Error: polar_angle = cls._default_polar_angle try: azimuthal_angle = cp.get_opt_tag(section, 'azimuthal-angle', tag) except Error: azimuthal_angle = cls._default_azimuthal_angle if polar_angle not in variable_args: raise Error("polar-angle %s is not one of the variable args (%s)"%( polar_angle, ', '.join(variable_args))) if azimuthal_angle not in variable_args: raise Error("azimuthal-angle %s is not one of the variable args "%( azimuthal_angle) + "(%s)"%(', '.join(variable_args))) # get the bounds, if provided polar_bounds = bounded.get_param_bounds_from_config( cp, section, tag, polar_angle) azimuthal_bounds = bounded.get_param_bounds_from_config( cp, section, tag, azimuthal_angle) # see if the a cyclic domain is desired for the azimuthal angle azimuthal_cyclic_domain = cp.has_option_tag(section, 'azimuthal_cyclic_domain', tag) return cls(polar_angle=polar_angle, azimuthal_angle=azimuthal_angle, polar_bounds=polar_bounds, azimuthal_bounds=azimuthal_bounds, azimuthal_cyclic_domain=azimuthal_cyclic_domain)
__all__ = ['UniformAngle', 'SinAngle', 'CosAngle', 'UniformSolidAngle']