From b921653a70a7187cccd195f40dfbe6470bfea280 Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Thu, 31 Oct 2024 16:27:38 -0400 Subject: [PATCH 1/6] added milky way height and radius distributions --- pycbc/distributions/__init__.py | 8 +- pycbc/distributions/milky_way_height.py | 108 ++++++++++++++++++++++++ pycbc/distributions/milky_way_radial.py | 94 +++++++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 pycbc/distributions/milky_way_height.py create mode 100644 pycbc/distributions/milky_way_radial.py diff --git a/pycbc/distributions/__init__.py b/pycbc/distributions/__init__.py index d6acccd1599..9f9cfb321b3 100644 --- a/pycbc/distributions/__init__.py +++ b/pycbc/distributions/__init__.py @@ -39,6 +39,10 @@ from pycbc.distributions.fixedsamples import FixedSamples from pycbc.distributions.mass import MchirpfromUniformMass1Mass2, \ QfromUniformMass1Mass2 +from pycbc.distributions.milky_way_radial import MilkyWayRadial +from pycbc.distributions.milky_way_height import MilkyWayHeight + + # a dict of all available distributions distribs = { @@ -61,7 +65,9 @@ FixedSamples.name: FixedSamples, MchirpfromUniformMass1Mass2.name: MchirpfromUniformMass1Mass2, QfromUniformMass1Mass2.name: QfromUniformMass1Mass2, - FisherSky.name: FisherSky + FisherSky.name: FisherSky, + MilkyWayHeight.name: MilkyWayHeight, + MilkyWayRadial.name: MilkyWayRadial } def read_distributions_from_config(cp, section="prior"): diff --git a/pycbc/distributions/milky_way_height.py b/pycbc/distributions/milky_way_height.py new file mode 100644 index 00000000000..926768965f6 --- /dev/null +++ b/pycbc/distributions/milky_way_height.py @@ -0,0 +1,108 @@ +import logging +import numpy +import scipy +from pycbc.distributions import bounded + +logger = logging.getLogger('pycbc.distributions.milky_way_height') + +class MilkyWayHeight(bounded.BoundedDist): + name = 'milky_way_height' + ''' + Returns the height (z-cordinate) in galactocentic frame following a symmetric exponential decay for 3 discs + Each disc reuires a weighting factor and a scale factor + + the config file should be + [prior-z] + name = milky_way_height + min-z = + max-z = + z_scale1 = + z_scale2 = + z_scale3 = + z_weight1 = + z_weight2 = + z_weight3 = + + ''' + def __init__(self, **params): + self._bounds = {} + self._scale1 = {} + self._scale2 = {} + self._scale3 = {} + self._weight1 = {} + self._weight2 = {} + self._weight3 = {} + self._norm = {} + self._lognorm = {} + ## Take out the elements in the params dict that ends with _scale by popping it out + ## so that the remaining elements can be passed to BoundedDist to create a bounded.dist + ## object we can call bounds from later. + scale1_args = [p for p in params if p.endswith('_scale1')] + self._scale1 = dict([p[:-7], params.pop(p)] for p in scale1_args) + scale2_args = [p for p in params if p.endswith('_scale2')] + self._scale2 = dict([p[:-7], params.pop(p)] for p in scale2_args) + scale3_args = [p for p in params if p.endswith('_scale3')] + self._scale3 = dict([p[:-7], params.pop(p)] for p in scale3_args) + ## + weight1_args = [p for p in params if p.endswith('_weight1')] + self._weight1 = dict([p[:-8], params.pop(p)] for p in weight1_args) + weight2_args = [p for p in params if p.endswith('_weight2')] + self._weight2 = dict([p[:-8], params.pop(p)] for p in weight2_args) + weight3_args = [p for p in params if p.endswith('_weight3')] + self._weight3 = dict([p[:-8], params.pop(p)] for p in weight3_args) + ## _scale keys removed from the params. + ## Now pass to bounded.dist + super(MilkyWayHeight, self).__init__(**params) + ## raising error if scale provided for param not in kwargs + #missing = set(self._scale.keys()) - set(params.keys()) + #if any(missing): + #raise ValueError(f"scales provided for unknown params {missing}") + + ## Set default scale to 1 if scale not provided for param in kwargs + #self._scale.update(dict([[p,1.] for p in params if p not in self._scale])) + + ## Calculate the norm and lognorm for each parameter and fill in the empty dics we created. + for p,bounds in self._bounds.items(): + self._norm[p] = 1. + self._lognorm[p] = -numpy.inf + + @property + def norm(self): + return 1. + def lognorm(self): + return -numpy.inf + + def _logpdf(self, **kwargs): + if kwargs in self: + return sum([numpy.log((self._weight1[p]/(2*self._scale1[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale1[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale1[p]) + + (self._weight2[p]/(2*self._scale2[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale2[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale2[p]) + + (self._weight3[p]/(2*self._scale3[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale3[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale3[p])) for p in self._params]) + else: + return -numpy.inf + def _pdf(self, **kwargs): + return numpy.e**(self._logpdf(**kwargs)) + + def _cdfinv_param(self, param, value): + z_min = self._bounds[param][0] + z_max = self._bounds[param][1] + z_d1 = self._scale1[param] + z_d2 = self._scale2[param] + z_d3 = self._scale3[param] + w1 = self._weight1[param] + w2 = self._weight2[param] + w3 = self._weight3[param] + beta1 = w1/(2*(1 - numpy.e**(z_min/z_d1))) + beta2 = w2/(2*(1 - numpy.e**(z_min/z_d2))) + beta3 = w3/(2*(1 - numpy.e**(z_min/z_d3))) + param_array = numpy.linspace(z_min, z_max, 500) + mask = param_array <= 0.0 + _cdf = numpy.zeros(len(param_array)) + _cdf[mask] = (beta1)*(numpy.e**(param_array[mask]/z_d1) - numpy.e**(z_min/z_d1)) + (beta2)*(numpy.e**(param_array[mask]/z_d2) - numpy.e**(z_min/z_d2)) + (beta3)*(numpy.e**(param_array[mask]/z_d3) - numpy.e**(z_min/z_d3)) + _cdf[~mask] = (beta1)*(2 - (numpy.e**(z_min/z_d1) + numpy.e**(-param_array[~mask]/z_d1))) + (beta2)*(2 - (numpy.e**(z_min/z_d2) + numpy.e**(-param_array[~mask]/z_d2))) + (beta3)*(2 - (numpy.e**(z_min/z_d3) + numpy.e**(-param_array[~mask]/z_d3))) + _cdfinv = scipy.interpolate.interp1d(_cdf, param_array) + return _cdfinv(value) + + # def from_config(cls, cp, section, variable_args): + # return super(SymExpDecay, cls).from_config(cp, section, variable_args, bounds_required = True) + +__all__ = ['MilkyWayHeight'] diff --git a/pycbc/distributions/milky_way_radial.py b/pycbc/distributions/milky_way_radial.py new file mode 100644 index 00000000000..cfcd80deadd --- /dev/null +++ b/pycbc/distributions/milky_way_radial.py @@ -0,0 +1,94 @@ +import logging +import numpy +import scipy +from pycbc.distributions import bounded + +logger = logging.getLogger('pycbc.distributions.milky_way_radial') + +class MilkyWayRadial(bounded.BoundedDist): + name = 'milky_way_radial' + ''' + Returns the radius (r-cordinate) in galactocentic frame following an exponential decaydistribution for 3 discs + Each disc reuires a weighting factor and a scale factor + + the config file should be + [prior-r] + name = milky_way_height + min-r = # Should not be negative + max-r = + r_scale1 = + r_scale2 = + r_scale3 = + r_weight1 = + r_weight2 = + r_weight3 = + + ''' + def __init__(self, **params): + self._bounds = {} + self._scale1 = {} + self._scale2 = {} + self._scale3 = {} + self._weight1 = {} + self._weight2 = {} + self._weight3 = {} + self._norm = {} + self._lognorm = {} + self._rarray = {} + self._cdfarray = {} + scale1_args = [p for p in params if p.endswith('_scale1')] + self._scale1 = dict([p[:-7], params.pop(p)] for p in scale1_args) + scale2_args = [p for p in params if p.endswith('_scale2')] + self._scale2 = dict([p[:-7], params.pop(p)] for p in scale2_args) + scale3_args = [p for p in params if p.endswith('_scale3')] + self._scale3 = dict([p[:-7], params.pop(p)] for p in scale3_args) + ## + weight1_args = [p for p in params if p.endswith('_weight1')] + self._weight1 = dict([p[:-8], params.pop(p)] for p in weight1_args) + weight2_args = [p for p in params if p.endswith('_weight2')] + self._weight2 = dict([p[:-8], params.pop(p)] for p in weight2_args) + weight3_args = [p for p in params if p.endswith('_weight3')] + self._weight3 = dict([p[:-8], params.pop(p)] for p in weight3_args) + ## _scale keys removed from the params. + ## Now pass to bounded.dist + super(MilkyWayRadial, self).__init__(**params) + for p, bounds in self._bounds.items(): + scale1 = self._scale1[p] + scale2 = self._scale2[p] + scale3 = self._scale3[p] + weight1 = self._weight1[p] + weight2 = self._weight2[p] + weight3 = self._weight3[p] + r_min, r_max = bounds + #if r_min < 0: + #raise ValueError(f'Minimum value of {p} must be greater than or equal to zero ') + r = numpy.linspace(r_min, r_max, 1000) + cdf_array = 1. - ((weight1*numpy.e**(-r/scale1)*(1+r/scale1))+ + (weight2*numpy.e**(-r/scale2)*(1+r/scale2))+ + (weight3*numpy.e**(-r/scale3)*(1+r/scale3))) + self._rarray[p] = r + self._cdfarray[p] = cdf_array + self._norm[p] = 1. + self._lognorm[p] = -numpy.inf + @property + def norm(self): + return 1. + def lognorm(self): + return numpy.inf + def _logpdf(self, **kwargs): + if kwargs in self: + return sum( + [ numpy.log(kwargs[p]*((self._weight1[p]*numpy.e**(-kwargs[p]/self._scale1[p])/self._scale1[p]**2) + +(self._weight2[p]*numpy.e**(-kwargs[p]/self._scale2[p])/self._scale2[p]**2) + +(self._weight3[p]*numpy.e**(-kwargs[p]/self._scale3[p])/self._scale3[p]**2))) for p in self._params]) + + else: + return -numpy.inf + + def _pdf(self, **kwargs): + return numpy.e**(_logpdf(**kwargs)) + def _cdfinv_param(self, param, value): + _interpolation = scipy.interpolate.interp1d(self._cdfarray[param], self._rarray[param]) + return _interpolation(value) + +__all__ = ['MilkWayRadial'] From d1be2bf09007d5ae1ab78d2e3d37cacaf958855a Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Fri, 15 Nov 2024 09:41:06 -0500 Subject: [PATCH 2/6] replaced two distribution files with one general distribution (symmetric gamma distribution) --- examples/distributions/example.ini | 10 ++ pycbc/distributions/__init__.py | 6 +- pycbc/distributions/milky_way_height.py | 108 --------------------- pycbc/distributions/milky_way_radial.py | 94 ------------------ pycbc/distributions/sym_gamma_dist.py | 121 ++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 206 deletions(-) delete mode 100644 pycbc/distributions/milky_way_height.py delete mode 100644 pycbc/distributions/milky_way_radial.py create mode 100644 pycbc/distributions/sym_gamma_dist.py diff --git a/examples/distributions/example.ini b/examples/distributions/example.ini index 5e6dc99dcec..4673038a71e 100644 --- a/examples/distributions/example.ini +++ b/examples/distributions/example.ini @@ -26,6 +26,7 @@ q = ;xi2 = ;phi_a = ;phi_s = +r = [prior-v0] name = uniform @@ -114,3 +115,12 @@ max-q = 8 ;max-chi_eff = 1 ;min-xi_bounds = 0. ;max-xi_bounds = 1 + +[prior-r] +name = sym_gamma_dist +min-r = -10 +max-r = 10 +scales = 1,2 +weights = 0.5,0.5 +power = 1 +interp_points = 1000 diff --git a/pycbc/distributions/__init__.py b/pycbc/distributions/__init__.py index 9f9cfb321b3..0ff61a2754f 100644 --- a/pycbc/distributions/__init__.py +++ b/pycbc/distributions/__init__.py @@ -39,8 +39,7 @@ from pycbc.distributions.fixedsamples import FixedSamples from pycbc.distributions.mass import MchirpfromUniformMass1Mass2, \ QfromUniformMass1Mass2 -from pycbc.distributions.milky_way_radial import MilkyWayRadial -from pycbc.distributions.milky_way_height import MilkyWayHeight +from pycbc.distributions.sym_gamma_dist import SymGammaDist @@ -66,8 +65,7 @@ MchirpfromUniformMass1Mass2.name: MchirpfromUniformMass1Mass2, QfromUniformMass1Mass2.name: QfromUniformMass1Mass2, FisherSky.name: FisherSky, - MilkyWayHeight.name: MilkyWayHeight, - MilkyWayRadial.name: MilkyWayRadial + SymGammaDist.name: SymGammaDist } def read_distributions_from_config(cp, section="prior"): diff --git a/pycbc/distributions/milky_way_height.py b/pycbc/distributions/milky_way_height.py deleted file mode 100644 index 926768965f6..00000000000 --- a/pycbc/distributions/milky_way_height.py +++ /dev/null @@ -1,108 +0,0 @@ -import logging -import numpy -import scipy -from pycbc.distributions import bounded - -logger = logging.getLogger('pycbc.distributions.milky_way_height') - -class MilkyWayHeight(bounded.BoundedDist): - name = 'milky_way_height' - ''' - Returns the height (z-cordinate) in galactocentic frame following a symmetric exponential decay for 3 discs - Each disc reuires a weighting factor and a scale factor - - the config file should be - [prior-z] - name = milky_way_height - min-z = - max-z = - z_scale1 = - z_scale2 = - z_scale3 = - z_weight1 = - z_weight2 = - z_weight3 = - - ''' - def __init__(self, **params): - self._bounds = {} - self._scale1 = {} - self._scale2 = {} - self._scale3 = {} - self._weight1 = {} - self._weight2 = {} - self._weight3 = {} - self._norm = {} - self._lognorm = {} - ## Take out the elements in the params dict that ends with _scale by popping it out - ## so that the remaining elements can be passed to BoundedDist to create a bounded.dist - ## object we can call bounds from later. - scale1_args = [p for p in params if p.endswith('_scale1')] - self._scale1 = dict([p[:-7], params.pop(p)] for p in scale1_args) - scale2_args = [p for p in params if p.endswith('_scale2')] - self._scale2 = dict([p[:-7], params.pop(p)] for p in scale2_args) - scale3_args = [p for p in params if p.endswith('_scale3')] - self._scale3 = dict([p[:-7], params.pop(p)] for p in scale3_args) - ## - weight1_args = [p for p in params if p.endswith('_weight1')] - self._weight1 = dict([p[:-8], params.pop(p)] for p in weight1_args) - weight2_args = [p for p in params if p.endswith('_weight2')] - self._weight2 = dict([p[:-8], params.pop(p)] for p in weight2_args) - weight3_args = [p for p in params if p.endswith('_weight3')] - self._weight3 = dict([p[:-8], params.pop(p)] for p in weight3_args) - ## _scale keys removed from the params. - ## Now pass to bounded.dist - super(MilkyWayHeight, self).__init__(**params) - ## raising error if scale provided for param not in kwargs - #missing = set(self._scale.keys()) - set(params.keys()) - #if any(missing): - #raise ValueError(f"scales provided for unknown params {missing}") - - ## Set default scale to 1 if scale not provided for param in kwargs - #self._scale.update(dict([[p,1.] for p in params if p not in self._scale])) - - ## Calculate the norm and lognorm for each parameter and fill in the empty dics we created. - for p,bounds in self._bounds.items(): - self._norm[p] = 1. - self._lognorm[p] = -numpy.inf - - @property - def norm(self): - return 1. - def lognorm(self): - return -numpy.inf - - def _logpdf(self, **kwargs): - if kwargs in self: - return sum([numpy.log((self._weight1[p]/(2*self._scale1[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale1[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale1[p]) - + (self._weight2[p]/(2*self._scale2[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale2[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale2[p]) - + (self._weight3[p]/(2*self._scale3[p]*(1 - numpy.e**(self._bounds[p][0]/self._scale3[p]))))*numpy.e**(-numpy.abs(kwargs[p])/self._scale3[p])) for p in self._params]) - else: - return -numpy.inf - def _pdf(self, **kwargs): - return numpy.e**(self._logpdf(**kwargs)) - - def _cdfinv_param(self, param, value): - z_min = self._bounds[param][0] - z_max = self._bounds[param][1] - z_d1 = self._scale1[param] - z_d2 = self._scale2[param] - z_d3 = self._scale3[param] - w1 = self._weight1[param] - w2 = self._weight2[param] - w3 = self._weight3[param] - beta1 = w1/(2*(1 - numpy.e**(z_min/z_d1))) - beta2 = w2/(2*(1 - numpy.e**(z_min/z_d2))) - beta3 = w3/(2*(1 - numpy.e**(z_min/z_d3))) - param_array = numpy.linspace(z_min, z_max, 500) - mask = param_array <= 0.0 - _cdf = numpy.zeros(len(param_array)) - _cdf[mask] = (beta1)*(numpy.e**(param_array[mask]/z_d1) - numpy.e**(z_min/z_d1)) + (beta2)*(numpy.e**(param_array[mask]/z_d2) - numpy.e**(z_min/z_d2)) + (beta3)*(numpy.e**(param_array[mask]/z_d3) - numpy.e**(z_min/z_d3)) - _cdf[~mask] = (beta1)*(2 - (numpy.e**(z_min/z_d1) + numpy.e**(-param_array[~mask]/z_d1))) + (beta2)*(2 - (numpy.e**(z_min/z_d2) + numpy.e**(-param_array[~mask]/z_d2))) + (beta3)*(2 - (numpy.e**(z_min/z_d3) + numpy.e**(-param_array[~mask]/z_d3))) - _cdfinv = scipy.interpolate.interp1d(_cdf, param_array) - return _cdfinv(value) - - # def from_config(cls, cp, section, variable_args): - # return super(SymExpDecay, cls).from_config(cp, section, variable_args, bounds_required = True) - -__all__ = ['MilkyWayHeight'] diff --git a/pycbc/distributions/milky_way_radial.py b/pycbc/distributions/milky_way_radial.py deleted file mode 100644 index cfcd80deadd..00000000000 --- a/pycbc/distributions/milky_way_radial.py +++ /dev/null @@ -1,94 +0,0 @@ -import logging -import numpy -import scipy -from pycbc.distributions import bounded - -logger = logging.getLogger('pycbc.distributions.milky_way_radial') - -class MilkyWayRadial(bounded.BoundedDist): - name = 'milky_way_radial' - ''' - Returns the radius (r-cordinate) in galactocentic frame following an exponential decaydistribution for 3 discs - Each disc reuires a weighting factor and a scale factor - - the config file should be - [prior-r] - name = milky_way_height - min-r = # Should not be negative - max-r = - r_scale1 = - r_scale2 = - r_scale3 = - r_weight1 = - r_weight2 = - r_weight3 = - - ''' - def __init__(self, **params): - self._bounds = {} - self._scale1 = {} - self._scale2 = {} - self._scale3 = {} - self._weight1 = {} - self._weight2 = {} - self._weight3 = {} - self._norm = {} - self._lognorm = {} - self._rarray = {} - self._cdfarray = {} - scale1_args = [p for p in params if p.endswith('_scale1')] - self._scale1 = dict([p[:-7], params.pop(p)] for p in scale1_args) - scale2_args = [p for p in params if p.endswith('_scale2')] - self._scale2 = dict([p[:-7], params.pop(p)] for p in scale2_args) - scale3_args = [p for p in params if p.endswith('_scale3')] - self._scale3 = dict([p[:-7], params.pop(p)] for p in scale3_args) - ## - weight1_args = [p for p in params if p.endswith('_weight1')] - self._weight1 = dict([p[:-8], params.pop(p)] for p in weight1_args) - weight2_args = [p for p in params if p.endswith('_weight2')] - self._weight2 = dict([p[:-8], params.pop(p)] for p in weight2_args) - weight3_args = [p for p in params if p.endswith('_weight3')] - self._weight3 = dict([p[:-8], params.pop(p)] for p in weight3_args) - ## _scale keys removed from the params. - ## Now pass to bounded.dist - super(MilkyWayRadial, self).__init__(**params) - for p, bounds in self._bounds.items(): - scale1 = self._scale1[p] - scale2 = self._scale2[p] - scale3 = self._scale3[p] - weight1 = self._weight1[p] - weight2 = self._weight2[p] - weight3 = self._weight3[p] - r_min, r_max = bounds - #if r_min < 0: - #raise ValueError(f'Minimum value of {p} must be greater than or equal to zero ') - r = numpy.linspace(r_min, r_max, 1000) - cdf_array = 1. - ((weight1*numpy.e**(-r/scale1)*(1+r/scale1))+ - (weight2*numpy.e**(-r/scale2)*(1+r/scale2))+ - (weight3*numpy.e**(-r/scale3)*(1+r/scale3))) - self._rarray[p] = r - self._cdfarray[p] = cdf_array - self._norm[p] = 1. - self._lognorm[p] = -numpy.inf - @property - def norm(self): - return 1. - def lognorm(self): - return numpy.inf - def _logpdf(self, **kwargs): - if kwargs in self: - return sum( - [ numpy.log(kwargs[p]*((self._weight1[p]*numpy.e**(-kwargs[p]/self._scale1[p])/self._scale1[p]**2) - +(self._weight2[p]*numpy.e**(-kwargs[p]/self._scale2[p])/self._scale2[p]**2) - +(self._weight3[p]*numpy.e**(-kwargs[p]/self._scale3[p])/self._scale3[p]**2))) for p in self._params]) - - else: - return -numpy.inf - - def _pdf(self, **kwargs): - return numpy.e**(_logpdf(**kwargs)) - def _cdfinv_param(self, param, value): - _interpolation = scipy.interpolate.interp1d(self._cdfarray[param], self._rarray[param]) - return _interpolation(value) - -__all__ = ['MilkWayRadial'] diff --git a/pycbc/distributions/sym_gamma_dist.py b/pycbc/distributions/sym_gamma_dist.py new file mode 100644 index 00000000000..43b57957732 --- /dev/null +++ b/pycbc/distributions/sym_gamma_dist.py @@ -0,0 +1,121 @@ +import logging +import numpy +import scipy +from pycbc.distributions import bounded + +logger = logging.getLogger('pycbc.distributions.sym_gamma_dist') + +class SymGammaDist(bounded.BoundedDist): + name = 'sym_gamma_dist' + """ + Samples from a weighted sum of one dimensional symmetric gamma distributions charecterised by a power $n$, + scale factors $s_{i}$, and weights $w_{i}$ + \[ + p(x) = \sum_{i} w_{i} |x|^{n} exp(-|x|/s_{i}) + \] + The inverse cdf is calculated by interpolating the cdf over the range of the variable + The cdf is calculated using regularised lower incomplete gamma function as defined in scipy.special + + Parameters + ----------- + Example config file + + [prior-x] + name = sym_gamma_dist + min-x = -10 + max-x = 10 + scales = 1,2 + weights = 0.5,0.5 + power = 2 + interp_points = 500 + + """ + + + + def __init__(self, scales = None, weights = None, + power = None, interp_points = None, **params): + super(SymGammaDist, self).__init__(**params) + self._scales = {} + self._weights = {} + self._power = {} + self._interp_points = {} + self._norms = {} + for p, bounds in self._bounds.items(): + if type(scales) == str: + self._scales[p] = [float(s) for s in scales.split(",")] + else: + self._scales[p] = [float(scales)] + if type(weights) == str: + self._weights[p] = [float(w) for w in weights.split(",")] + else: + self._weights[p] = [float(weights)] + self._power[p] = float(power) + self._interp_points[p] = int(interp_points) + if len(self._scales[p]) != len(self._weights[p]): + raise ValueError(f'Unequal number of scales and weights' + + f'provided for parameter {p}') + if numpy.sum(self._weights[p]) != float(1): + raise ValueError('Weights should add to 1.0') + self._norms[p] = [0]*len(self._scales[p]) + if numpy.sign(bounds[0]) == numpy.sign(bounds[1]): + for i in range(len(self._norms[p])): + self._norms[p][i] = numpy.abs(scipy.special.factorial(self._power[p]) + *self._scales[p][i]**(self._power[p]+1) + *(scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[0]/self._scales[p][i])) + -scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[1]/self._scales[p][i])))) + if numpy.sign(bounds[0]) != numpy.sign(bounds[1]): + for i in range(len(self._norms[p])): + self._norms[p][i] = numpy.abs(scipy.special.factorial(self._power[p]) + *self._scales[p][i]**(self._power[p]+1) + *(scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[0]/self._scales[p][i])) + +scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[1]/self._scales[p][i])))) + + def c0(self, x, power, scale): + return scipy.special.gammainc(power+1,numpy.abs(x/scale))*\ + scipy.special.factorial(power)*scale**(power+1) + + def _logpdf(self, **kwargs): + if kwargs in self: + lpdf_p = 0 + for p in self._params: + pdf_i = 0 + for i in range(len(self._scales[p])): + pdf_i += (self._weights[p][i]/self._norms[p][i])* \ + numpy.abs(kwargs[p])**(self._power[p])* \ + numpy.e**(-numpy.abs(kwargs[p])/self._scales[p][i]) + lpdf_p += numpy.log(pdf_i) + return lpdf_p + else: + return -numpy.inf + def _pdf(self, **kwargs): + return numpy.e**(self._logpdf(**kwargs)) + + def _cdfinv_param(self, param, value): + param_min, param_max = self._bounds[param][0], self._bounds[param][1] + param_array = numpy.linspace(param_min, + param_max, + self._interp_points[param]) + + if numpy.sign(param_min) != numpy.sign(param_max): + cdf_array = numpy.zeros(self._interp_points[param]) + for i in range(len(self._scales[param])): + mask = param_array <= 0 + cdf_array[mask] = cdf_array[mask] + (self._weights[param][i]/self._norms[param][i])* \ + (self.c0(numpy.abs(param_min), self._power[param], self._scales[param][i]) + -self.c0(numpy.abs(param_array[mask]), self._power[param], self._scales[param][i])) + cdf_array[~mask] = cdf_array[~mask] + (self._weights[param][i]/self._norms[param][i])* \ + (self.c0(param_min, self._power[param], self._scales[param][i]) + +self.c0(param_array[~mask], self._power[param], self._scales[param][i])) + inv_cdf_interpolate = scipy.interpolate.interp1d(cdf_array, param_array) + + return inv_cdf_interpolate(value) + if numpy.sign(param_min) == numpy.sign(param_max): + cdf_array = numpy.zeros(self._interp_points[param]) + for i in range(len(self._scales[param])): + cdf_array += (self._weights[param][i]/self._norms[param][i])* \ + numpy.abs(self.c0(param_min, self._power[param], self._scales[param][i]) + -self.c0(param_array, self._power[param], self._scales[param][i])) + inv_cdf_interpolate = scipy.interpolate.interp1d(cdf_array, param_array) + return inv_cdf_interpolate(value) +__all__ = ['SymGammaDist'] From 65c77d0588539c48b7dda77403eccc0464edd0ca Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Wed, 20 Nov 2024 14:15:31 -0500 Subject: [PATCH 3/6] Corrected code duplications and updated docstring --- examples/distributions/example.ini | 6 +- pycbc/distributions/sym_gamma_dist.py | 189 +++++++++++++++----------- 2 files changed, 109 insertions(+), 86 deletions(-) diff --git a/examples/distributions/example.ini b/examples/distributions/example.ini index 4673038a71e..c1f97c55335 100644 --- a/examples/distributions/example.ini +++ b/examples/distributions/example.ini @@ -120,7 +120,7 @@ max-q = 8 name = sym_gamma_dist min-r = -10 max-r = 10 -scales = 1,2 -weights = 0.5,0.5 -power = 1 +scales = 3 5 +weights = 1 3 +power = 2 interp_points = 1000 diff --git a/pycbc/distributions/sym_gamma_dist.py b/pycbc/distributions/sym_gamma_dist.py index 43b57957732..eb6821e1a4d 100644 --- a/pycbc/distributions/sym_gamma_dist.py +++ b/pycbc/distributions/sym_gamma_dist.py @@ -2,88 +2,135 @@ import numpy import scipy from pycbc.distributions import bounded +from numpy import sign, abs +from scipy.special import factorial, gammainc +from scipy.interpolate import interp1d logger = logging.getLogger('pycbc.distributions.sym_gamma_dist') class SymGammaDist(bounded.BoundedDist): - name = 'sym_gamma_dist' - """ - Samples from a weighted sum of one dimensional symmetric gamma distributions charecterised by a power $n$, - scale factors $s_{i}$, and weights $w_{i}$ - \[ - p(x) = \sum_{i} w_{i} |x|^{n} exp(-|x|/s_{i}) - \] - The inverse cdf is calculated by interpolating the cdf over the range of the variable - The cdf is calculated using regularised lower incomplete gamma function as defined in scipy.special + r"""Samples from a weighted sum of one dimensional symmetric gamma + distributions between the bounds provided. + The PDF is given by + .. math:: + p(x) = \sum_{i} w_{i} |x|^{n} e^{\frac{-|x|}{s_{i}}} + + where :math:'w_{i}' are the weights, :math:'n' is the power, + and :math:'s_{i}' are the scale factors of the individual distributions + + The CDF between the bounds :math:'[a,b]' is given by + .. math:: + c(r) = \sum_{i} w_{i} \int_{a}^{r} |x|^{n} e^{\frac{-|x|}{s_{i}}} + + The CDF is calculated using a rescaling of scipy's implementation of + regularised lower incomplete gamma function gammainc + :math:'\gamma(n+1,\frac{r}{s_{i}})' as follows + + ..math:: + c(r) = |\gamma(n+1,\frac{r}{s_{i}}) - \sigma(r,b)\gamma(n+1,\frac{b}{s_{i}})| + + where :math:'\sigma(u, v) = 1' if sign(u)=sign(v) else -1. + The inverse cdf is calculated by interpolating the cdf over the + range of the parameter. The number of points over which the + interpolation is done is 5000 by default. + Parameters ----------- + Bounds : The minimum and maximum range of the parameter. Can be + provided as bounded.dist object + scales : The scale factors for the individual distributions. Must be + provided as a space separated list + weights : The weighting factors for the individual distributions. Must + be provided as a space separated list. Can provide unnormalized + weights. Defaults to 1 if only one distribution is used. + power : The power for the gamma distribution. + interp_points : The number of points over which the CDF is evaluated + to interpolate the inverse CDF + Example config file [prior-x] name = sym_gamma_dist min-x = -10 max-x = 10 - scales = 1,2 - weights = 0.5,0.5 + scales = 3 5 + weights = 1 3 power = 2 - interp_points = 500 - + interp_points = 1000 """ + name = 'sym_gamma_dist' - - def __init__(self, scales = None, weights = None, - power = None, interp_points = None, **params): + def __init__(self, scales=None, weights=None, + power=None, interp_points=5000, **params): super(SymGammaDist, self).__init__(**params) - self._scales = {} - self._weights = {} - self._power = {} - self._interp_points = {} + + if isinstance(scales, str): + self._scales = [float(s) for s in scales.split()] + else: + self._scales = [float(scales)] + + if isinstance(weights, str): + weight_floats = [float(w) for w in weights.split()] + self._weights = [w/sum(weight_floats) for w in weight_floats] + else: + self._weights = [float(1.0)] + + if len(self._scales) != len(self._weights): + raise ValueError("Unequal number of scales and weights provided") + + self._power = float(power) + self._interp_points = int(interp_points) + self._interpolated_invcdf = {} self._norms = {} for p, bounds in self._bounds.items(): - if type(scales) == str: - self._scales[p] = [float(s) for s in scales.split(",")] - else: - self._scales[p] = [float(scales)] - if type(weights) == str: - self._weights[p] = [float(w) for w in weights.split(",")] - else: - self._weights[p] = [float(weights)] - self._power[p] = float(power) - self._interp_points[p] = int(interp_points) - if len(self._scales[p]) != len(self._weights[p]): - raise ValueError(f'Unequal number of scales and weights' - + f'provided for parameter {p}') - if numpy.sum(self._weights[p]) != float(1): - raise ValueError('Weights should add to 1.0') - self._norms[p] = [0]*len(self._scales[p]) - if numpy.sign(bounds[0]) == numpy.sign(bounds[1]): - for i in range(len(self._norms[p])): - self._norms[p][i] = numpy.abs(scipy.special.factorial(self._power[p]) - *self._scales[p][i]**(self._power[p]+1) - *(scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[0]/self._scales[p][i])) - -scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[1]/self._scales[p][i])))) - if numpy.sign(bounds[0]) != numpy.sign(bounds[1]): - for i in range(len(self._norms[p])): - self._norms[p][i] = numpy.abs(scipy.special.factorial(self._power[p]) - *self._scales[p][i]**(self._power[p]+1) - *(scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[0]/self._scales[p][i])) - +scipy.special.gammainc(self._power[p]+1, numpy.abs(bounds[1]/self._scales[p][i])))) - - def c0(self, x, power, scale): - return scipy.special.gammainc(power+1,numpy.abs(x/scale))*\ - scipy.special.factorial(power)*scale**(power+1) - + lower, upper = bounds[0], bounds[1] + param_array = numpy.linspace(lower, upper, self._interp_points) + cdf_array = numpy.zeros(len(param_array)) + norms = numpy.zeros(len(self._scales)) + for i in range(len(self._scales)): + norms[i] = self.integral_gamma( + lower, upper, self._power, self._scales[i] + ) + cdf_array += ( + self._weights[i] + *self.integral_gamma( + lower,param_array,self._power,self._scales[i] + ) + /norms[i] + ) + self._interpolated_invcdf[p] = interp1d(cdf_array,param_array) + self._norms[p] = norms + + + def rescaled_gammainc(self, x, power, scale): + """ Rescales the lower incomplete gamma function + """ + return gammainc(power+1,abs(x/scale))*\ + factorial(power)*scale**(power+1) + + def integral_gamma(self,lower,upper,power,scale): + """ The definite integral of the indivdual PDF between the + limits 'lower' and 'upper' + """ + lower,upper = numpy.asarray(lower), numpy.asarray(upper) + rescaled_upper = self.rescaled_gammainc(upper, power, scale) + rescaled_lower = self.rescaled_gammainc(lower, power, scale) + sign_factor = numpy.where(sign(lower)==sign(upper),1.0,-1.0) + return numpy.abs(rescaled_upper - sign_factor*rescaled_lower) + + + def _logpdf(self, **kwargs): if kwargs in self: lpdf_p = 0 for p in self._params: pdf_i = 0 - for i in range(len(self._scales[p])): - pdf_i += (self._weights[p][i]/self._norms[p][i])* \ - numpy.abs(kwargs[p])**(self._power[p])* \ - numpy.e**(-numpy.abs(kwargs[p])/self._scales[p][i]) + for i in range(len(self._scales)): + pdf_i += (self._weights[i]/self._norms[p][i])* \ + abs(kwargs[p])**(self._power)* \ + numpy.e**(-abs(kwargs[p])/self._scales[i]) lpdf_p += numpy.log(pdf_i) return lpdf_p else: @@ -92,30 +139,6 @@ def _pdf(self, **kwargs): return numpy.e**(self._logpdf(**kwargs)) def _cdfinv_param(self, param, value): - param_min, param_max = self._bounds[param][0], self._bounds[param][1] - param_array = numpy.linspace(param_min, - param_max, - self._interp_points[param]) - - if numpy.sign(param_min) != numpy.sign(param_max): - cdf_array = numpy.zeros(self._interp_points[param]) - for i in range(len(self._scales[param])): - mask = param_array <= 0 - cdf_array[mask] = cdf_array[mask] + (self._weights[param][i]/self._norms[param][i])* \ - (self.c0(numpy.abs(param_min), self._power[param], self._scales[param][i]) - -self.c0(numpy.abs(param_array[mask]), self._power[param], self._scales[param][i])) - cdf_array[~mask] = cdf_array[~mask] + (self._weights[param][i]/self._norms[param][i])* \ - (self.c0(param_min, self._power[param], self._scales[param][i]) - +self.c0(param_array[~mask], self._power[param], self._scales[param][i])) - inv_cdf_interpolate = scipy.interpolate.interp1d(cdf_array, param_array) - - return inv_cdf_interpolate(value) - if numpy.sign(param_min) == numpy.sign(param_max): - cdf_array = numpy.zeros(self._interp_points[param]) - for i in range(len(self._scales[param])): - cdf_array += (self._weights[param][i]/self._norms[param][i])* \ - numpy.abs(self.c0(param_min, self._power[param], self._scales[param][i]) - -self.c0(param_array, self._power[param], self._scales[param][i])) - inv_cdf_interpolate = scipy.interpolate.interp1d(cdf_array, param_array) - return inv_cdf_interpolate(value) + invcdf = self._interpolated_invcdf[param] + return invcdf(value) __all__ = ['SymGammaDist'] From 02bb816835ba2db9435a5720c160931b65737694 Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Thu, 28 Nov 2024 14:39:04 -0500 Subject: [PATCH 4/6] fixed docstring for math equations plus minor pep8 style changes --- pycbc/distributions/sym_gamma_dist.py | 41 +++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/pycbc/distributions/sym_gamma_dist.py b/pycbc/distributions/sym_gamma_dist.py index eb6821e1a4d..207b462fd8a 100644 --- a/pycbc/distributions/sym_gamma_dist.py +++ b/pycbc/distributions/sym_gamma_dist.py @@ -13,21 +13,21 @@ class SymGammaDist(bounded.BoundedDist): distributions between the bounds provided. The PDF is given by .. math:: - p(x) = \sum_{i} w_{i} |x|^{n} e^{\frac{-|x|}{s_{i}}} + p(x) = \sum_{i} w_{i} |x|^{n} e^{\frac{-|x|}{s_{i}}} where :math:'w_{i}' are the weights, :math:'n' is the power, and :math:'s_{i}' are the scale factors of the individual distributions The CDF between the bounds :math:'[a,b]' is given by .. math:: - c(r) = \sum_{i} w_{i} \int_{a}^{r} |x|^{n} e^{\frac{-|x|}{s_{i}}} + c(r) = \sum_{i} w_{i} \int_{a}^{r} |x|^{n} e^{\frac{-|x|}{s_{i}}} The CDF is calculated using a rescaling of scipy's implementation of regularised lower incomplete gamma function gammainc :math:'\gamma(n+1,\frac{r}{s_{i}})' as follows ..math:: - c(r) = |\gamma(n+1,\frac{r}{s_{i}}) - \sigma(r,b)\gamma(n+1,\frac{b}{s_{i}})| + c(r) = |\gamma(n+1,\frac{r}{s_{i}}) - \sigma(r,b)\gamma(n+1,\frac{b}{s_{i}})| where :math:'\sigma(u, v) = 1' if sign(u)=sign(v) else -1. @@ -62,16 +62,16 @@ class SymGammaDist(bounded.BoundedDist): name = 'sym_gamma_dist' - def __init__(self, scales=None, weights=None, - power=None, interp_points=5000, **params): + def __init__(self,scales=None,weights=None, + power=None,interp_points=5000,**params): super(SymGammaDist, self).__init__(**params) - if isinstance(scales, str): + if isinstance(scales,str): self._scales = [float(s) for s in scales.split()] else: self._scales = [float(scales)] - if isinstance(weights, str): + if isinstance(weights,str): weight_floats = [float(w) for w in weights.split()] self._weights = [w/sum(weight_floats) for w in weight_floats] else: @@ -86,12 +86,12 @@ def __init__(self, scales=None, weights=None, self._norms = {} for p, bounds in self._bounds.items(): lower, upper = bounds[0], bounds[1] - param_array = numpy.linspace(lower, upper, self._interp_points) + param_array = numpy.linspace(lower,upper,self._interp_points) cdf_array = numpy.zeros(len(param_array)) norms = numpy.zeros(len(self._scales)) for i in range(len(self._scales)): norms[i] = self.integral_gamma( - lower, upper, self._power, self._scales[i] + lower,upper,self._power,self._scales[i] ) cdf_array += ( self._weights[i] @@ -103,8 +103,7 @@ def __init__(self, scales=None, weights=None, self._interpolated_invcdf[p] = interp1d(cdf_array,param_array) self._norms[p] = norms - - def rescaled_gammainc(self, x, power, scale): + def rescaled_gammainc(self,x,power,scale): """ Rescales the lower incomplete gamma function """ return gammainc(power+1,abs(x/scale))*\ @@ -114,15 +113,13 @@ def integral_gamma(self,lower,upper,power,scale): """ The definite integral of the indivdual PDF between the limits 'lower' and 'upper' """ - lower,upper = numpy.asarray(lower), numpy.asarray(upper) - rescaled_upper = self.rescaled_gammainc(upper, power, scale) - rescaled_lower = self.rescaled_gammainc(lower, power, scale) + lower, upper = numpy.asarray(lower), numpy.asarray(upper) + rescaled_upper = self.rescaled_gammainc(upper,power,scale) + rescaled_lower = self.rescaled_gammainc(lower,power,scale) sign_factor = numpy.where(sign(lower)==sign(upper),1.0,-1.0) return numpy.abs(rescaled_upper - sign_factor*rescaled_lower) - - - - def _logpdf(self, **kwargs): + + def _logpdf(self,**kwargs): if kwargs in self: lpdf_p = 0 for p in self._params: @@ -135,10 +132,12 @@ def _logpdf(self, **kwargs): return lpdf_p else: return -numpy.inf - def _pdf(self, **kwargs): + def _pdf(self,**kwargs): return numpy.e**(self._logpdf(**kwargs)) - def _cdfinv_param(self, param, value): + def _cdfinv_param(self,param,value): invcdf = self._interpolated_invcdf[param] return invcdf(value) -__all__ = ['SymGammaDist'] + + +__all__ = ['SymGammaDist'] \ No newline at end of file From 5cad1576284d561903995826f3098f322cface38 Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Thu, 28 Nov 2024 15:10:48 -0500 Subject: [PATCH 5/6] fixed docstring for math equations --- pycbc/distributions/sym_gamma_dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycbc/distributions/sym_gamma_dist.py b/pycbc/distributions/sym_gamma_dist.py index 207b462fd8a..e8ac3bd9cef 100644 --- a/pycbc/distributions/sym_gamma_dist.py +++ b/pycbc/distributions/sym_gamma_dist.py @@ -26,7 +26,7 @@ class SymGammaDist(bounded.BoundedDist): regularised lower incomplete gamma function gammainc :math:'\gamma(n+1,\frac{r}{s_{i}})' as follows - ..math:: + .. math:: c(r) = |\gamma(n+1,\frac{r}{s_{i}}) - \sigma(r,b)\gamma(n+1,\frac{b}{s_{i}})| where :math:'\sigma(u, v) = 1' if sign(u)=sign(v) else -1. From 0e781d8816a2ca1345e9a389c46f2cfdfa3f25d1 Mon Sep 17 00:00:00 2001 From: vikasjadhav-why Date: Thu, 28 Nov 2024 15:46:26 -0500 Subject: [PATCH 6/6] replaced tabs with spaces, changed doctring to comments in the class methods --- pycbc/distributions/sym_gamma_dist.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pycbc/distributions/sym_gamma_dist.py b/pycbc/distributions/sym_gamma_dist.py index e8ac3bd9cef..5cf6ccb4b11 100644 --- a/pycbc/distributions/sym_gamma_dist.py +++ b/pycbc/distributions/sym_gamma_dist.py @@ -104,15 +104,13 @@ def __init__(self,scales=None,weights=None, self._norms[p] = norms def rescaled_gammainc(self,x,power,scale): - """ Rescales the lower incomplete gamma function - """ + #Rescales the lower incomplete gamma function return gammainc(power+1,abs(x/scale))*\ factorial(power)*scale**(power+1) def integral_gamma(self,lower,upper,power,scale): - """ The definite integral of the indivdual PDF between the - limits 'lower' and 'upper' - """ + # The definite integral of the indivdual PDF between the + # limits 'lower' and 'upper' lower, upper = numpy.asarray(lower), numpy.asarray(upper) rescaled_upper = self.rescaled_gammainc(upper,power,scale) rescaled_lower = self.rescaled_gammainc(lower,power,scale)