diff --git a/gstlal-inspiral/python/stats/Makefile.am b/gstlal-inspiral/python/stats/Makefile.am index aed36e7aacdbf4a4a7aac90a23eda2247fbfae49..0bc6cd2ef6b9d241e7a481aff01524134db299f2 100644 --- a/gstlal-inspiral/python/stats/Makefile.am +++ b/gstlal-inspiral/python/stats/Makefile.am @@ -4,6 +4,7 @@ statsdir = $(pkgpythondir)/stats stats_PYTHON = \ horizonhistory.py \ inspiral_extrinsics.py \ + inspiral_intrinsics.py \ inspiral_lr.py \ trigger_rate.py diff --git a/gstlal-inspiral/python/stats/inspiral_intrinsics.py b/gstlal-inspiral/python/stats/inspiral_intrinsics.py new file mode 100644 index 0000000000000000000000000000000000000000..fe4ccc6020ad3f66edb4144a569ef13b6fec4bed --- /dev/null +++ b/gstlal-inspiral/python/stats/inspiral_intrinsics.py @@ -0,0 +1,108 @@ +# Copyright (C) 2017,2018 Heather Fong +# +# 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 2 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. + + +# +# ============================================================================= +# +# Preamble +# +# ============================================================================= +# + + +import h5py +import math +import numpy +import os +from scipy.interpolate import PPoly + + +from gstlal import stats as gstlalstats + + +# FIXME: caution, this information might get organized differently later. +# for now we just need to figure out where the gstlal-inspiral directory in +# share/ is. don't write anything that assumes that this module will +# continue to define any of these symbols +from gstlal import paths as gstlal_config_paths + + +__all__ = [ + "UniformInTemplatePopulationModel", + "SourcePopulationModel" +] + + +# +# ============================================================================= +# +# Population Model +# +# ============================================================================= +# + + +class UniformInTemplatePopulationModel(object): + def __init__(self, template_ids): + """ + Assumes uniform in template population model, no + astrophysical prior. + """ + self.lnP = -math.log(len(template_ids)) + + + @gstlalstats.assert_ln_probability + def lnP_template_signal(self, template_id, snr): + assert snr >= 0. + return self.lnP + + +class SourcePopulationModel(object): + DEFAULT_FILENAME = os.path.join(gstlal_config_paths["pkgdatadir"], "lnP_template_signal_BNS_gaussian_lowspin_Ozel.hdf5") + + + def __init__(self, template_ids, filename = None): + """ + Sets the polynomial coefficients, given the template ID + and SNR for a source population model, from which + lnP is then computed using PPoly. + """ + with h5py.File(filename if filename is not None else self.DEFAULT_FILENAME, 'r') as model: + coefficients = model['coefficients'].value + snr_bp = model['SNR'].value + # PPoly can construct an array of polynomials by just + # feeding it the coefficients array all in one go, but then + # it insists on evaluating all of them at once. we don't + # want to suffer that cost, so we have to make an array of + # PPoly objects ourselves, and index into it to evaluate + # just one. since we have to do this anyway, we use a + # dictionary to also solve the problem of mapping + # template_id to a specific polynomial + self.polys = dict((template_id, PPoly(coefficients[:,:,[template_id]], snr_bp)) for template_id in template_ids) + self.max_snr = snr_bp.max() + + + @gstlalstats.assert_ln_probability + def lnP_template_signal(self, template_id, snr): + assert snr >= 0. + try: + lnP_vs_snr = self.polys[template_id] + except KeyError: + raise KeyError("template ID %d is not in this model" % template_id) + # PPoly's .__call__() returns an array, so we need the + # final [0] to flatten it + return lnP_vs_snr(min(snr, self.max_snr))[0] diff --git a/gstlal-inspiral/python/stats/inspiral_lr.py b/gstlal-inspiral/python/stats/inspiral_lr.py index 5add6472f6d9bcac507883a63192a2884ca06a7f..3fad027f0cd805bf813384f2116dbe7eabd57a3c 100644 --- a/gstlal-inspiral/python/stats/inspiral_lr.py +++ b/gstlal-inspiral/python/stats/inspiral_lr.py @@ -44,6 +44,7 @@ from glue.ligolw import utils as ligolw_utils from glue import segments from gstlal.stats import horizonhistory from gstlal.stats import inspiral_extrinsics +from gstlal.stats import inspiral_intrinsics from gstlal.stats import trigger_rate import lal from lal import rate @@ -259,6 +260,17 @@ class LnSignalDensity(LnLRDensity): # record of horizon distances for all instruments in the # network self.horizon_history = horizonhistory.HorizonHistories((instrument, horizonhistory.NearestLeafTree()) for instrument in self.instruments) + + # source population model + if self.template_ids: + self.population_model = inspiral_intrinsics.UniformInTemplatePopulationModel(self.template_ids) + # FIXME: switch to this when a model file becomes + # available + #self.population_model = inspiral_intrinsics.SourcePopulationModel(self.template_ids) + else: + # default lnP = 1/len(templates) = 0 + self.population_model = inspiral_intrinsics.UniformInTemplatePopulationModel([0]) + self.InspiralExtrinsics = inspiral_extrinsics.InspiralExtrinsics(self.min_instruments) def __call__(self, segments, snrs, phase, dt, template_id, **kwargs): @@ -310,6 +322,9 @@ class LnSignalDensity(LnLRDensity): except ValueError: return float("-inf") + # evaluate population model + lnP += self.population_model.lnP_template_signal(template_id, max(snrs.values())) + # evalute the (snr, \chi^2 | snr) PDFs (same for all # instruments) interp = self.interps["snr_chi"] @@ -475,6 +490,19 @@ class LnSignalDensity(LnLRDensity): xml = cls.get_xml_root(xml, name) self = super(LnSignalDensity, cls).from_xml(xml, name) self.horizon_history = horizonhistory.HorizonHistories.from_xml(xml, u"horizon_history") + # source population model + # FIXME: this should probably be stored in the ranking + # statistic file somehow. maybe the HDF5 filename could be + # stored. whatever would allow the correct model to be + # re-initialized + if self.template_ids: + self.population_model = inspiral_intrinsics.UniformInTemplatePopulationModel(self.template_ids) + # FIXME: switch to this when a model file becomes + # available + #self.population_model = inspiral_intrinsics.SourcePopulationModel(self.template_ids) + else: + # default lnP = 1/len(templates) = 0 + self.population_model = inspiral_intrinsics.UniformInTemplatePopulationModel([0]) return self @@ -519,6 +547,9 @@ class DatalessLnSignalDensity(LnSignalDensity): except ValueError: return float("-inf") + # evaluate population model + lnP += self.population_model.lnP_template_signal(template_id, max(snrs.values())) + # evalute the (snr, \chi^2 | snr) PDFs (same for all # instruments) interp = self.interps["snr_chi"]