New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add light curve upper limits #1456
Changes from 2 commits
5578fcf
6e12a25
524337a
e084f3b
6c13ad6
c44edb9
98e3659
81302f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
'significance_on_off', | ||
'excess_matching_significance', | ||
'excess_matching_significance_on_off', | ||
'Helene_ULs', | ||
] | ||
|
||
__doctest_skip__ = ['*'] | ||
|
@@ -413,6 +414,84 @@ def _significance_direct_on_off(n_on, n_off, alpha): | |
|
||
return significance | ||
|
||
def Helene_ULs(excess, error, conf_level=95.45): | ||
"""Compute Upper limit based on Helene et al., NIM 212 (1983) 319 | ||
|
||
Parameters | ||
---------- | ||
excess : double | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Write always "float" instead of "double", that's what the type is called in Python. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, a too fast copy/paste. Corrected |
||
Signal excess | ||
error : double | ||
Gaussian Signal error | ||
conf_level: double | ||
Confidence level in percentage (1sigma=68.27, 2sigma=95.45, 3sigma=99.73, 5sigma=99.999943) | ||
|
||
Returns | ||
------- | ||
double | ||
Upper limit for the excess | ||
""" | ||
|
||
conf_level1 = 1. - conf_level/100. | ||
|
||
if error <= 0: | ||
return 0. | ||
|
||
from math import sqrt | ||
from scipy.special import erf | ||
if excess >= 0.: | ||
zeta = excess/error | ||
value = zeta/sqrt(2.) | ||
integral = (1.+erf(value))/2. | ||
integral2 = 1. - conf_level1*integral | ||
value_old = value | ||
|
||
# The 1st Loop is for Speed & 2nd For Precision | ||
value_new = value_old+0.01 | ||
if integral > integral2: | ||
value_new = 0. | ||
integral = (1.+erf(value_new))/2. | ||
|
||
while integral < integral2: | ||
value_old = value_new | ||
value_new = value_new+0.01 | ||
integral = (1.+erf(value_new))/2. | ||
value_new = value_old+0.0000001 | ||
integral = (1. + erf(value_new))/2. | ||
|
||
while integral < integral2: | ||
value_old = value_new | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PyCharm shows me that this This code is really hard to understand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment on value_old is accepted and its removal is done. |
||
value_new = value_new+0.0000001 | ||
integral = (1.+erf(value_new))/2. | ||
value_new = value_new*sqrt(2.) | ||
conf_limit = (value_new+zeta)*error | ||
return conf_limit | ||
elif excess < 0.: | ||
zeta = -excess/error | ||
value = zeta/sqrt(2.) | ||
integral = 1 - (1.+erf(value))/2. | ||
integral2 = 1. - conf_level1*integral | ||
value_old = value | ||
|
||
# The 1st Loop is for Speed & 2nd For Precision | ||
value_new = value_old+0.01 | ||
integral = (1.+erf(value_new))/2. | ||
while integral < integral2: | ||
value_old = value_new | ||
value_new = value_new+0.01 | ||
integral = (1.+erf(value_new))/2. | ||
value_new = value_old+0.0000001 | ||
integral = (1.+erf(value_new))/2. | ||
|
||
while integral < integral2: | ||
value_old = value_new | ||
value_new = value_new+0.0000001 | ||
integral = (1.+erf(value_new))/2. | ||
value_new = value_new*sqrt(2.) | ||
conf_limit = (value_new-zeta)*error | ||
return conf_limit | ||
else: | ||
return 0. | ||
|
||
def excess_matching_significance(mu_bkg, significance, method='lima'): | ||
r"""Compute excess matching a given significance. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
significance_on_off, | ||
excess_matching_significance, | ||
excess_matching_significance_on_off, | ||
Helene_ULs, | ||
) | ||
|
||
pytest.importorskip('scipy') | ||
|
@@ -37,6 +38,9 @@ def test_excess_error(): | |
assert_allclose(excess_error(n_on=10, n_off=20, alpha=0.1), 3.1937439) | ||
assert_allclose(excess_error(n_on=4, n_off=9, alpha=0.5), 2.5) | ||
|
||
def test_Helene_ULs(): | ||
assert_allclose(Helene_ULs(excess=50, error=40, conf_level=99.73), 119.708038) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get a different value in the test?!
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the paper has plenty of "reference values" on what the result should be? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using this command, I see that the
Please add at least one test case, preferably using a test case given in the paper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did a stupid typo. Thanks a lot to have re-check that. Corrected. |
||
assert_allclose(Helene_ULs(excess=10, error=6, conf_level=99.73), 20.280005) | ||
|
||
def test_significance(): | ||
# Check that the Li & Ma limit formula is correct | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
from astropy.table import Table | ||
from astropy.time import Time | ||
from ..spectrum.utils import CountsPredictor | ||
from ..stats.poisson import excess_error | ||
from ..stats.poisson import excess_error, Helene_ULs | ||
from ..utils.scripts import make_path | ||
from ..stats.poisson import significance_on_off | ||
|
||
|
@@ -642,7 +642,7 @@ def make_time_intervals_min_significance(self, significance, significance_method | |
table['t_stop'] = Time(table['t_stop'], format='mjd', scale='tt') | ||
return table | ||
|
||
def light_curve(self, time_intervals, spectral_model, energy_range): | ||
def light_curve(self, time_intervals, spectral_model, energy_range, sigma_ul_thres=3.): | ||
"""Compute light curve. | ||
|
||
Implementation follows what is done in: | ||
|
@@ -659,6 +659,8 @@ def light_curve(self, time_intervals, spectral_model, energy_range): | |
Spectral model | ||
energy_range : `~astropy.units.Quantity` | ||
True energy range to evaluate integrated flux (true energy) | ||
sigma_ul_thres : float | ||
Threshold on detection significance to compute Flux Upper Limits | ||
|
||
Returns | ||
------- | ||
|
@@ -667,7 +669,7 @@ def light_curve(self, time_intervals, spectral_model, energy_range): | |
""" | ||
rows = [] | ||
for time_interval in time_intervals: | ||
useinterval, row = self.compute_flux_point(time_interval, spectral_model, energy_range) | ||
useinterval, row = self.compute_flux_point(time_interval, spectral_model, energy_range, sigma_ul_thres) | ||
if useinterval: | ||
rows.append(row) | ||
|
||
|
@@ -691,7 +693,7 @@ def _make_lc_from_row_data(rows): | |
|
||
return LightCurve(table) | ||
|
||
def compute_flux_point(self, time_interval, spectral_model, energy_range): | ||
def compute_flux_point(self, time_interval, spectral_model, energy_range, sigma_ul_thres=3.): | ||
"""Compute one flux point for one time interval. | ||
|
||
Parameters | ||
|
@@ -702,6 +704,8 @@ def compute_flux_point(self, time_interval, spectral_model, energy_range): | |
Spectral model | ||
energy_range : `~astropy.units.Quantity` | ||
True energy range to evaluate integrated flux (true energy) | ||
sigma_ul_thres : float | ||
Threshold on detection significance to compute Flux Upper Limits | ||
|
||
Returns | ||
------- | ||
|
@@ -800,20 +804,31 @@ def compute_flux_point(self, time_interval, spectral_model, energy_range): | |
|
||
flux = measured_excess / predicted_excess.value | ||
flux *= int_flux | ||
flux_err = int_flux / predicted_excess.value | ||
|
||
# Gaussian errors, TODO: should be improved | ||
flux_err *= excess_error(n_on=n_on, n_off=n_off, alpha=alpha_mean) | ||
flux_err = int_flux / predicted_excess.value | ||
delta_excess = excess_error(n_on=n_on, n_off=n_off, alpha=alpha_mean) | ||
flux_err *= delta_excess | ||
|
||
flux_ul = -1 | ||
sigma = significance_on_off(n_on=n_on, n_off=n_off, alpha=alpha_mean, method='lima') | ||
if sigma <= sigma_ul_thres: | ||
flux_ul = Helene_ULs(n_on - alpha_mean * n_off, delta_excess, 99.73) #3 sigma ULs | ||
flux_ul *= int_flux / predicted_excess.value | ||
else: | ||
flux = 0 | ||
flux_err = 0 | ||
flux_ul = -1 | ||
sigma = 0. | ||
|
||
# Store measurements in a dict and return that | ||
return useinterval, OrderedDict([ | ||
('time_min', Time(tmin, format='mjd')), | ||
('time_max', Time(tmax, format='mjd')), | ||
('flux', flux * u.Unit('1 / (s cm2)')), | ||
('flux_err', flux_err * u.Unit('1 / (s cm2)')), | ||
|
||
('flux_ul', flux_ul * u.Unit('1 / (s cm2)')), | ||
('is_ul', sigma <= sigma_ul_thres) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bkhelifi - You're missing a comma here:
________________________________________________________________________ test_lightcurve_estimator ________________________________________________________________________
gammapy/time/tests/test_lightcurve.py:206: gammapy/time/lightcurve.py:672: in light_curve self = <gammapy.time.lightcurve.LightCurveEstimator object at 0xb147e2da0>
E TypeError: 'tuple' object is not callable gammapy/time/lightcurve.py:832: TypeError
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course! My configuration has not run all the tests, in particular the one of the class LightCurve. I missed indeed bugs, showing that the test functions are useful! |
||
('livetime', livetime * u.s), | ||
('alpha', alpha_mean), | ||
('n_on', n_on), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually give a URL to the paper in the docstring.
I think it's this one?
http://adsabs.harvard.edu/abs/1984NIMPA.228..120H
Can you please add it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done