gstlal 1.13.0
Loading...
Searching...
No Matches
__init__.py
Go to the documentation of this file.
1# Copyright (C) 2011--2014 Kipp Cannon, Chad Hanna, Drew Keppel
2# Copyright (C) 2013 Jacob Peoples
3#
4# This program is free software; you can redistribute it and/or modify it
5# under the terms of the GNU General Public License as published by the
6# Free Software Foundation; either version 2 of the License, or (at your
7# option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12# Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18
32
33# - Address the fixed SNR PDF using median PSD which could be pre-computed and stored on disk. (Store database of SNR pdfs for a variety of horizon)
34# - The binning parameters are hard-coded too; Could it be a problem?
35# - Chisquare binning hasn't been tuned to be a good representation of the PDFs; could be improved in future
36
37
38
39
40#
41# =============================================================================
42#
43# Preamble
44#
45# =============================================================================
46#
47
48
49try:
50 from fpconst import NaN, NegInf, PosInf
51except ImportError:
52 # fpconst is not part of the standard library and might not be
53 # available
54 NaN = float("nan")
55 NegInf = float("-inf")
56 PosInf = float("+inf")
57import itertools
58import math
59import numpy
60from scipy.special import ive
61
62
63#
64# ============================================================================
65#
66# Non-central chisquared pdf
67#
68# ============================================================================
69#
70
71#
72# FIXME this is to work around a precision issue in scipy
73# See: https://github.com/scipy/scipy/issues/1608
74#
75
76def logiv(v, z):
77 # from Abramowitz and Stegun (9.7.1), if mu = 4 v^2, then for large
78 # z:
79 #
80 # Iv(z) ~= exp(z) / (\sqrt(2 pi z)) { 1 - (mu - 1)/(8 z) + (mu - 1)(mu - 9) / (2! (8 z)^2) - (mu - 1)(mu - 9)(mu - 25) / (3! (8 z)^3) ... }
81 # Iv(z) ~= exp(z) / (\sqrt(2 pi z)) { 1 + (mu - 1)/(8 z) [-1 + (mu - 9) / (2 (8 z)) [1 - (mu - 25) / (3 (8 z)) ... ]]}
82 # log Iv(z) ~= z - .5 log(2 pi) log z + log1p((mu - 1)/(8 z) (-1 + (mu - 9)/(16 z) (1 - (mu - 25)/(24 z) ... )))
83
84 with numpy.errstate(divide = "ignore"):
85 a = numpy.log(ive(v,z))
86
87 # because this result will only be used for large z, to silence
88 # divide-by-0 complaints from inside log1p() when z is small we
89 # clip z to 1.
90 mu = 4. * v**2.
91 with numpy.errstate(divide = "ignore", invalid = "ignore"):
92 b = -math.log(2. * math.pi) / 2. * numpy.log(z)
93 z = numpy.clip(z, 1., PosInf)
94 b += numpy.log1p((mu - 1.) / (8. * z) * (-1. + (mu - 9.) / (16. * z) * (1. - (mu - 25.) / (24. * z))))
95
96 return z + numpy.where(z < 1e8, a, b)
97
98# See: http://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution
99def ncx2logpdf(x, k, l):
100 return -math.log(2.) - (x+l)/2. + (k/4.-.5) * (numpy.log(x) - numpy.log(l)) + logiv(k/2.-1., numpy.sqrt(l) * numpy.sqrt(x))
101
102def ncx2pdf(x, k, l):
103 return numpy.exp(ncx2logpdf(x, k, l))
104
105
106#
107# =============================================================================
108#
109# Poisson Stuff
110#
111# =============================================================================
112#
113
114
115def assert_probability(f):
116 def g(*args, **kwargs):
117 p = f(*args, **kwargs)
118 if isinstance(p, numpy.ndarray):
119 assert ((0. <= p) & (p <= 1.)).all()
120 else:
121 assert 0. <= p <= 1.
122 return p
123 return g
124
125
126def assert_ln_probability(f):
127 def g(*args, **kwargs):
128 p = f(*args, **kwargs)
129 if isinstance(p, numpy.ndarray):
130 assert (p <= 0.).all()
131 else:
132 assert p <= 0.
133 return p
134 return g
135
136
137@assert_probability
138@numpy.vectorize
139def poisson_p_not_0(l):
140 """
141 Return the probability that a Poisson process with a mean rate of l
142 yields a non-zero count. = 1 - exp(-l), but computed in a way that
143 avoids loss of precision and over-/underflows.
144
145 Example:
146
147 >>> print(poisson_p_not_0(1e+60))
148 1.0
149 >>> print(poisson_p_not_0(1)
150 0.632120558829
151 >>> print(poisson_p_not_0(1e-60))
152 1e-60
153 >>> print(poisson_p_not_0(0))
154 0.0
155 """
156 assert l >= 0.
157
158 # need -l everywhere. also make sure we haven't been passed an
159 # integer as that will mess up the arithmetic in the Taylor
160 # expansion below
161
162 l = -float(l)
163
164 #
165 # result is closer to 1 than to 0. use direct evaluation.
166 #
167
168 if l < -0.69314718055994529:
169 return 1. - math.exp(l)
170
171 #
172 # result is closer to 0 than to 1. use Taylor expansion for exp()
173 # with leading term removed to avoid having to subtract from 1 to
174 # get answer.
175 #
176 # 1 - exp x = -(x + x^2/2! + x^3/3! + ...)
177 #
178
179 s = [l]
180 term = l
181 threshold = -1e-20 * l
182 for n in itertools.count(2):
183 term *= l / n
184 s.append(term)
185 if abs(term) <= threshold:
186 # smallest term was added last, want to compute sum
187 # from smallest to largest
188 s.reverse()
189 s = -sum(s)
190 # if the sum was identically 0 then we've ended up
191 # with -0.0 which we add a special case for to make
192 # positive.
193 assert s >= 0.
194 return s if s else 0.
195
196
197@assert_probability
198def poisson_p_0(l):
199 """
200 Return the probability that a Poisson process with a mean rate of l
201 yields a zero count. = exp(-l), but with a sanity check that l is
202 non-negative.
203 """
204 assert l >= 0.
205 return numpy.exp(-l)
206
207
208@assert_ln_probability
209def poisson_ln_p_0(l):
210 """
211 Return the natural logarithm of the probability that a Poisson
212 process with a mean rate of l yields a zero count. = -l, but with
213 a sanity check that l is non-negative.
214 """
215 assert l >= 0.
216 return -l
217
218
219#
220# =============================================================================
221#
222# Binomial Stuff
223#
224# =============================================================================
225#
226
227
228@assert_probability
229@numpy.vectorize
230def fap_after_trials(p, m):
231 """
232 Given the probability, p, that an event occurs, compute the
233 probability of at least one such event occuring after m independent
234 trials.
235
236 The return value is 1 - (1 - p)^m computed avoiding round-off
237 errors and underflows. m cannot be negative but need not be an
238 integer.
239
240 Example:
241
242 >>> fap_after_trials(0.5, 1)
243 0.5
244 >>> fap_after_trials(0.066967008463192584, 10)
245 0.5
246 >>> fap_after_trials(0.0069075045629640984, 100)
247 0.5
248 >>> fap_after_trials(0.00069290700954747807, 1000)
249 0.5
250 >>> fap_after_trials(0.000069312315846428086, 10000)
251 0.5
252 >>> fap_after_trials(.000000006931471781576803, 100000000)
253 0.5
254 >>> fap_after_trials(0.000000000000000069314718055994534, 10000000000000000)
255 0.5
256 >>> "%.15g" % fap_after_trials(0.1, 21.854345326782834)
257 '0.9'
258 >>> "%.15g" % fap_after_trials(1e-17, 2.3025850929940458e+17)
259 '0.9'
260 >>> fap_after_trials(0.1, .2)
261 0.020851637639023216
262 """
263 # when m*p is not >> 1, we can use an expansion in powers of m*p
264 # that allows us to avoid having to compute differences of
265 # probabilities (which would lead to loss of precision when working
266 # with probabilities close to 0 or 1)
267 #
268 # 1 - (1 - p)^m = m p - (m^2 - m) p^2 / 2 +
269 # (m^3 - 3 m^2 + 2 m) p^3 / 6 -
270 # (m^4 - 6 m^3 + 11 m^2 - 6 m) p^4 / 24 + ...
271 #
272 # the coefficient of each power of p is a polynomial in m. if m <<
273 # 1, these polynomials can be approximated by their leading term in
274 # m
275 #
276 # 1 - (1 - p)^m ~= m p + m p^2 / 2 + m p^3 / 3 + m p^4 / 4 + ...
277 # = -m * log(1 - p)
278 #
279 # NOTE: the ratio of the coefficients of higher-order terms in the
280 # polynomials in m to that of the leading-order term grows with
281 # successive powers of p and eventually the higher-order terms will
282 # dominate. we assume that this does not happen until the power of
283 # p is sufficiently high that the entire term (polynomial in m and
284 # all) is negligable and the series has been terminated.
285 #
286 # if m is not << 1, then returning to the full expansion, starting
287 # at 0, the nth term in the series is
288 #
289 # -1^n * (m - 0) * (m - 1) * ... * (m - n) * p^(n + 1) / (n + 1)!
290 #
291 # if the (n-1)th term is X, the nth term in the series is
292 #
293 # X * (n - m) * p / (n + 1)
294 #
295 # this recursion relation allows us to compute successive terms
296 # without explicit evaluation of the full numerator and denominator
297 # expressions (each of which quickly overflow).
298 #
299 # for sufficiently large n the denominator dominates and the terms
300 # in the series become small and the sum eventually converges to a
301 # stable value (and if m is an integer the sum is exact in a finite
302 # number of terms). however, if m*p >> 1 it can take many terms
303 # before the series sum stabilizes, terms in the series initially
304 # grow large and alternate in sign and an accurate result can only
305 # be obtained through careful cancellation of the large values.
306 #
307 # for large m*p we write the expression as
308 #
309 # 1 - (1 - p)^m = 1 - exp(m log(1 - p))
310 #
311 # if p is small, log(1 - p) suffers from loss of precision but the
312 # Taylor expansion of log(1 - p) converges quickly
313 #
314 # m ln(1 - p) = -m p - m p^2 / 2 - m p^3 / 3 - ...
315 # = -m p * (1 + p / 2 + p^2 / 3 + ...)
316 #
317 # math libraries (including Python's) generally provide log1p(),
318 # which evalutes log(1 + p) accurately for small p. we rely on
319 # this function to provide results valid both in the small-p and
320 # not small-p regimes.
321 #
322 # if p = 1, log1p() complains about an overflow error. we trap
323 # these and return the hard-coded answer
324 #
325
326 if m <= 0.:
327 raise ValueError("m = %g must be positive" % m)
328 if not (0. <= p <= 1.):
329 raise ValueError("p = %g must be between 0 and 1 inclusively" % p)
330
331 if m * p < 4.:
332 #
333 # expansion of 1 - (1 - p)^m in powers of p
334 #
335
336 if m < 1e-8:
337 #
338 # small m approximation
339 #
340
341 try:
342 return -m * math.log1p(-p)
343 except OverflowError:
344 #
345 # p is too close to 1. result is 1
346 #
347
348 return 1.
349
350 #
351 # general m
352 #
353
354 s = []
355 term = -1.
356 for n in itertools.count():
357 term *= (n - m) * p / (n + 1.)
358 s.append(term)
359 # 0th term is always positive
360 if abs(term) <= 1e-18 * s[0]:
361 s.reverse()
362 return sum(s)
363
364 #
365 # compute result as 1 - exp(m * log(1 - p)). use poisson_p_not_0()
366 # to evaluate 1 - exp() with an algorithm that avoids loss of
367 # precision.
368 #
369
370 try:
371 x = m * math.log1p(-p)
372 except OverflowError:
373 #
374 # p is very close to 1. we know p <= 1 because it's a
375 # probability, and we know that m*p >= 4 otherwise we
376 # wouldn't have followed this code path, and so because p
377 # is close to 1 and m is not small we can safely assume the
378 # anwer is 1.
379 #
380
381 return 1.
382
383 return float(poisson_p_not_0(-x))
384
385
386def trials_from_faps(p0, p1):
387 """
388 Given the probabiity, p0, of an event occuring, and the
389 probability, p1, of at least one such event being observed after
390 some number of independent trials, solve for and return the number
391 of trials, m, that relates the two probabilities. The three
392 quantities are related by p1 = 1 - (1 - p0)^m. Generally the
393 return value is not an integer.
394
395 See also fap_after_trials(). Note that if p0 is 0 or 1 then p1
396 must be 0 or 1 respectively, and in both cases m is undefined.
397 Otherwise if p1 is 1 then inf is returned.
398 """
399 assert 0. <= p0 <= 1. # p0 must be a valid probability
400 assert 0. <= p1 <= 1. # p1 must be a valid probability
401
402 if p0 == 0. or p0 == 1.:
403 assert p0 == p1 # require valid relationship
404 # but we still can't solve for m
405 raise ValueError("m undefined")
406 if p1 == 1.:
407 return PosInf
408
409 #
410 # m log(1 - p0) = log(1 - p1)
411 #
412
413 return math.log1p(-p1) / math.log1p(-p0)