diff --git a/gstlal-inspiral/python/stats/inspiral_extrinsics.py b/gstlal-inspiral/python/stats/inspiral_extrinsics.py
index 19cb07e8816b458723b94842ec039f068c64b04c..09e6c5775d6bad6eb333002332012f89bd8e70af 100644
--- a/gstlal-inspiral/python/stats/inspiral_extrinsics.py
+++ b/gstlal-inspiral/python/stats/inspiral_extrinsics.py
@@ -43,6 +43,7 @@ from scipy import stats
 from scipy import spatial
 import sys
 import h5py
+import healpy
 
 from glue.ligolw import ligolw
 from glue.ligolw import lsctables
@@ -939,17 +940,21 @@ class NumeratorSNRCHIPDF(rate.BinnedLnPDF):
 # =============================================================================
 #
 
+
 class TPhiDeffPDF(object):
 	def __init__(self):
 		DEFAULT_FILENAME = os.path.join(gstlal_config_paths["pkgdatadir"], "inspiral_dtdphi_pdf.h5")
 		self.IE = InspiralExtrinsics.from_hdf5(DEFAULT_FILENAME)
 
+
 def chunker(seq, size):
 	return (seq[pos:pos + size] for pos in xrange(0, len(seq), size))
-	
+
+
 def normsq_along_one(x):
 	return numpy.add.reduce(x * x, axis=(1,))
 
+
 def margprob(Dmat):
 	out = []
 	for D in Dmat:
@@ -962,14 +967,15 @@ def margprob(Dmat):
 			out.append(step * scipy.integrate.simps(numpy.exp(-D**2/2.)))
 	return out
 
+
 class InspiralExtrinsics(object):
 
-	def __init__(self, tree_data = None, margsky = None):
+	# NOTE to save a couple hundred megs of ram we do not
+	# include kagra for now...
+	responses = {"H1": lal.CachedDetectors[lal.LHO_4K_DETECTOR].response, "L1":lal.CachedDetectors[lal.LLO_4K_DETECTOR].response, "V1":lal.CachedDetectors[lal.VIRGO_DETECTOR].response}#, "K1":lal.CachedDetectors[lal.KAGRA_DETECTOR].response}
+	locations = {"H1":lal.CachedDetectors[lal.LHO_4K_DETECTOR].location, "L1":lal.CachedDetectors[lal.LLO_4K_DETECTOR].location, "V1":lal.CachedDetectors[lal.VIRGO_DETECTOR].location}#, "K1":lal.CachedDetectors[lal.KAGRA_DETECTOR].location}
 
-		# NOTE to save a couple hundred megs of ram we do not
-		# include kagra for now...
-		self.responses = {"H1": lal.CachedDetectors[lal.LHO_4K_DETECTOR].response, "L1":lal.CachedDetectors[lal.LLO_4K_DETECTOR].response, "V1":lal.CachedDetectors[lal.VIRGO_DETECTOR].response}#, "K1":lal.CachedDetectors[lal.KAGRA_DETECTOR].response}
-		self.locations = {"H1":lal.CachedDetectors[lal.LHO_4K_DETECTOR].location, "L1":lal.CachedDetectors[lal.LLO_4K_DETECTOR].location, "V1":lal.CachedDetectors[lal.VIRGO_DETECTOR].location}#, "K1":lal.CachedDetectors[lal.KAGRA_DETECTOR].location}
+	def __init__(self, tree_data = None, margsky = None):
 
 		# FIXME compute this more reliably or expose it as a property
 		# or something
@@ -979,10 +985,9 @@ class InspiralExtrinsics(object):
 		self.margsky = margsky
 
 		if self.tree_data is None:
-			time, phase, deff = self.tile()
+			time, phase, deff = InspiralExtrinsics.tile()
 			self.tree_data = self.dtdphideffpoints(time, phase, deff, self.slices)
 
-
 		# produce KD trees for all the combinations.  NOTE we slice
 		# into the same array for memory considerations.  the KDTree
 		# does *not* make copies of the data so be careful to not
@@ -1002,7 +1007,7 @@ class InspiralExtrinsics(object):
 				slcs = sorted(sum(self.instrument_pair_slices(self.instrument_pairs(combo)).values(),[]))
 				#
 				# NOTE we approximate the marginalization
-				# integral with 10% of the sky points 
+				# integral with 10% of the sky points
 				#
 				num_points = int(self.tree_data.shape[0] / 10.)
 
@@ -1011,12 +1016,12 @@ class InspiralExtrinsics(object):
 				# in the reference platforms, but this doesn't
 				# get used in practice during an actual
 				# analysis.  This will use 8GB of RAM and keep
-				# a box pretty busy. 
+				# a box pretty busy.
 				for points in chunker(self.tree_data[:,slcs], 1000):
 					Dmat = self.KDTree[combo].query(points, k=num_points, distance_upper_bound = 3.5, n_jobs=-1)[0]
 					marg.extend(margprob(Dmat))
 				self.margsky[combo] = numpy.array(marg, dtype="float32")
-				
+
 	def to_hdf5(self, fname):
 		f = h5py.File(fname, "w")
 		dgrp = f.create_group("gstlal_extparams")
@@ -1027,7 +1032,7 @@ class InspiralExtrinsics(object):
 		f.close()
 
 	@staticmethod
-	def from_hdf5(fname):	
+	def from_hdf5(fname):
 		f = h5py.File(fname, "r")
 		dgrp = f["gstlal_extparams"]
 		tree_data = numpy.array(dgrp["treedata"])
@@ -1056,13 +1061,13 @@ class InspiralExtrinsics(object):
 	def instrument_pair_slices(self, pairs):
 		s = self.slices
 		return dict((pair, s[pair]) for pair in pairs)
-				
-	def instrument_combos(self, instruments):
+
+	@classmethod
+	def instrument_combos(cls, instruments, min_instruments = 2):
 		combos = set()
 		# FIXME this probably should be exposed, but 1 doesn't really make sense anyway
-		min_instruments = 2
-		for i in range(min_instruments, len(self.responses) + 1):
-			for choice in itertools.combinations(self.responses, i):
+		for i in range(min_instruments, len(instruments,) + 1):
+			for choice in itertools.combinations(instruments, i):
 				# NOTE the reference IFO is always the first in
 				# alphabetical order for any given combination,
 				# hence the sort here
@@ -1095,13 +1100,8 @@ class InspiralExtrinsics(object):
 
 		return out
 
-	def tile(self):
-		# NOTE an OK low res approximation
-		#NSIDE = 8
-		#NANGLE = 17 
-		# NOTE a very good high res approximation
-		NSIDE = 16
-		NANGLE = 33 
+	@classmethod
+	def tile(cls, NSIDE = 16, NANGLE = 33):
 		healpix_idxs = numpy.arange(healpy.nside2npix(NSIDE))
 		# We are concerned with a shell on the sky at some fiducial
 		# time (which simply fixes Earth as a natural coordinate
@@ -1109,9 +1109,9 @@ class InspiralExtrinsics(object):
 		T = lal.LIGOTimeGPS(0)
 		GMST = lal.GreenwichMeanSiderealTime(T)
 		D = 1.
-		phase = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in self.responses)
-		deff = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in self.responses)
-		time = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in self.responses)
+		phase = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in cls.responses)
+		deff = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in cls.responses)
+		time = dict((ifo, numpy.zeros(len(healpix_idxs) * NANGLE**2, dtype="float32")) for ifo in cls.responses)
 
 		print "tiling sky: \n"
 		cnt = 0
@@ -1123,11 +1123,11 @@ class InspiralExtrinsics(object):
 				hplus = 0.5 * (1.0 + COSIOTA**2)
 				hcross = COSIOTA
 				for PSI in numpy.linspace(0, numpy.pi * 2, NANGLE):
-					for ifo in self.responses:
-						Fplus, Fcross = lal.ComputeDetAMResponse(self.responses[ifo], RA, DEC, PSI, GMST)
+					for ifo in cls.responses:
+						Fplus, Fcross = lal.ComputeDetAMResponse(cls.responses[ifo], RA, DEC, PSI, GMST)
 						phase[ifo][cnt] = -numpy.arctan2(Fcross * hcross, Fplus * hplus)
 						deff[ifo][cnt] = D / ((Fplus * hplus)**2 + (Fcross * hcross)**2)**0.5
-						time[ifo][cnt] = lal.TimeDelayFromEarthCenter(self.locations[ifo], RA, DEC, T)
+						time[ifo][cnt] = lal.TimeDelayFromEarthCenter(cls.locations[ifo], RA, DEC, T)
 					cnt += 1
 
 		print "\n...done"
@@ -1144,3 +1144,110 @@ class InspiralExtrinsics(object):
 		D2 = numpy.dot(D,D)
 		return numpy.exp(-D2 / 2.) * self.margsky[combo][nearestix] / self.norm
 
+
+class p_of_instruments_given_horizons(object):
+	def __init__(self, instruments = ("H1", "L1", "V1"), snr_thresh = 4., nbins = 41, hmin = 0.05, hmax = 20.0, histograms = None):
+		self.instruments = tuple(sorted(list(instruments)))
+		self.snr_thresh = snr_thresh
+		self.nbins = nbins
+		self.hmin = hmin
+		self.hmax = hmax
+		# NOTE should be sorted
+
+		if histograms is not None:
+			self.histograms = histograms
+		else:
+			combos = inspiral_extrinsics.InspiralExtrinsics.instrument_combos(self.instruments, min_instruments = 1)
+			self.histograms = {}
+			bins = []
+			for i in range(len(self.instruments) - 1):
+				bins.append(rate.LogarithmicBins(self.hmin, self.hmax, self.nbins))
+			for combo in combos:
+				print combo
+				self.histograms[combo] = rate.BinnedArray(rate.NDBins(bins))
+
+			_, _, deff = inspiral_extrinsics.InspiralExtrinsics.tile(NSIDE = 8, NANGLE = 17)
+			alldeff = []
+			for v in deff.values():
+				alldeff.extend(v)
+			mindeff = min(alldeff)
+			maxdeff = max(alldeff)
+			print mindeff, maxdeff
+
+			for horizontuple in itertools.product(*[b.centres() for b in bins]):
+				horizondict = {}
+				# NOTE by defn the first instrument in alpha order will always have a horizon of 1
+				horizondict[self.instruments[0]] = 1.0
+				for i, ifo in enumerate(self.instruments[1:]):
+					horizondict[ifo] = horizontuple[i]
+				snrs = {}
+				snrs_above_thresh = {}
+				snrs_below_thresh = {}
+				prob = []
+				for cnt, ifo in enumerate(horizondict):
+					# FIXME remember to carefully comment this logic
+					LOW = self.hmin * 8. / self.snr_thresh / maxdeff
+					HIGH = max(horizontuple + (1,)) * 8. / self.snr_thresh / mindeff
+					for D in numpy.linspace(LOW, HIGH, 200):
+						#blah = 8 * horizondict[ifo] / (D * deff[ifo])
+						#print ifo, len(blah), D, 8 * horizondict[ifo], len(blah[blah > 4])
+						snrs.setdefault(ifo,[]).extend(8 * horizondict[ifo] / (D * deff[ifo]))
+						if cnt == 0:
+							prob.extend([D**2] * len(deff[ifo]))
+					snrs[ifo] = stats.ncx2.rvs(2, numpy.array(snrs[ifo])**2)**.5
+					snrs_above_thresh[ifo] = snrs[ifo] >= self.snr_thresh
+					snrs_below_thresh[ifo] = snrs[ifo] < self.snr_thresh
+					print horizontuple, ifo, len(snrs_above_thresh[ifo][snrs_above_thresh[ifo]])
+					prob = numpy.array(prob)
+				total = 0.
+				for combo in combos:
+					for cnt, ifo in enumerate(combo):
+						if cnt == 0:
+							must_be_above = snrs_above_thresh[ifo].copy()
+						else:
+							must_be_above &= snrs_above_thresh[ifo]
+					# the ones above thresh must be accompanied with the compliment to this combo being below thresh
+					for ifo in set(self.instruments) - set(combo):
+						must_be_above &= snrs_below_thresh[ifo]
+					# sum up the prob
+					count = sum(prob[must_be_above])
+					self.histograms[combo][horizontuple] += count
+					total += count
+				for I in self.histograms:
+					self.histograms[I][horizontuple] /= total
+					print horizontuple, I, self.histograms[I][horizontuple]
+
+	def __call__(self, instruments, horizon_distances):
+		H = [horizon_distances[k] for k in sorted(horizon_distances)]
+		return self.histograms[tuple(sorted(instruments))][[h / H[0] for h in H[1:]]]
+
+	def to_hdf5(self, fname):
+		f = h5py.File(fname, "w")
+		grp = f.create_group("gstlal_p_of_instruments")
+		grp.attrs["snr_thresh"] = self.snr_thresh
+		grp.attrs["hmin"] = self.hmin
+		grp.attrs["hmax"] = self.hmax
+		grp.attrs["nbins"] = self.nbins
+		grp.attrs["instruments"] = ",".join(self.instruments)
+		for combo in self.histograms:
+			grp.create_dataset(",".join(combo), data = self.histograms[combo].array, compression="gzip")
+		f.close()
+
+	@staticmethod
+	def from_hdf5(fname):
+		f = h5py.File(fname, "r")
+		grp = f["gstlal_p_of_instruments"]
+		snr_thresh = grp.attrs["snr_thresh"]
+		hmin = grp.attrs["hmin"]
+		hmax = grp.attrs["hmax"]
+		nbins = grp.attrs["nbins"]
+		instruments = tuple(sorted(grp.attrs["instruments"].split(",")))
+		histograms = {}
+		bins = []
+		for i in range(len(instruments) - 1):
+			bins.append(rate.LogarithmicBins(hmin, hmax, nbins))
+		for combo in inspiral_extrinsics.InspiralExtrinsics.instrument_combos(instruments, min_instruments = 1):
+			histograms[combo] = rate.BinnedArray(rate.NDBins(bins))
+			histograms[combo].array[:] = numpy.array(grp[",".join(combo)])[:]
+		f.close()
+		return p_of_instruments_given_horizons(instruments = instruments, snr_thresh = snr_thresh, nbins = nbins, hmin = hmin, hmax = hmax, histograms = histograms)