Skip to content

Commit 1a83239

Browse files
authored
Merge pull request #109 from fact-project/units
Use astropy units consistently, fix spectral_index madness
2 parents 13c6619 + 59df237 commit 1a83239

File tree

6 files changed

+176
-65
lines changed

6 files changed

+176
-65
lines changed

fact/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.19.1
1+
0.20.0

fact/analysis/statistics.py

+145-51
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,92 @@
11
import numpy as np
2+
import astropy.units as u
23

4+
POINT_SOURCE_FLUX_UNIT = (1 / u.GeV / u.s / u.m**2).unit
5+
FLUX_UNIT = POINT_SOURCE_FLUX_UNIT / u.sr
36

4-
def random_power(spectral_index, e_min, e_max, size):
7+
8+
@u.quantity_input
9+
def random_power(
10+
spectral_index,
11+
e_min: u.TeV,
12+
e_max: u.TeV,
13+
size,
14+
e_ref: u.TeV=1 * u.TeV,
15+
) -> u.TeV:
516
r'''
617
Draw random numbers from a power law distribution
718
819
.. math::
920
f(E) =
1021
\frac{1 - \gamma}
11-
{E_{\max}^{1 - \gamma} - E_{\min}^{1 - \gamma}}
12-
E^{-\gamma}
22+
({E_{\max} / E_{\mathrm{ref}})^{\gamma - 1} - (E_{\min} / E_{\mathrm{ref}})^{\gamma - 1}}
23+
(E / E_{\mathrm{ref}})^{\gamma}
1324
1425
Parameters
1526
----------
1627
spectral_index: float
1728
The differential spectral index of the power law
18-
e_min: float
29+
e_min: Quantity[energy]
1930
lower energy border
20-
e_max: float
31+
e_max: Quantity[energy]
2132
upper energy border, can be np.inf
2233
size: int or tuple[int]
2334
Number of events to draw or a shape like (100, 2)
2435
'''
25-
assert spectral_index > 1.0, 'spectral_index must be > 1.0'
36+
if spectral_index > -1.0:
37+
raise ValueError('spectral_index must be < -1.0')
38+
2639
u = np.random.uniform(0, 1, size)
2740

28-
exponent = 1 - spectral_index
41+
exponent = spectral_index + 1
2942

3043
if e_max == np.inf:
31-
diff = -e_min**exponent
44+
diff = -(e_min / e_ref)**exponent
3245
else:
33-
diff = e_max**exponent - e_min**exponent
46+
diff = (e_max / e_ref)**exponent - (e_min / e_ref)**exponent
3447

35-
return (diff * u + e_min**exponent) ** (1 / exponent)
48+
return e_ref * (diff * u + (e_min / e_ref)**exponent) ** (1 / exponent)
3649

3750

38-
def power_law(energy, flux_normalization, spectral_index):
51+
def power_law(
52+
energy: u.TeV,
53+
flux_normalization: (FLUX_UNIT, POINT_SOURCE_FLUX_UNIT),
54+
spectral_index,
55+
e_ref: u.TeV=1 * u.TeV,
56+
):
3957
r'''
4058
Simple power law
4159
4260
.. math::
43-
\phi = \phi_0 \cdot E ^{-\gamma}
61+
\phi = \phi_0 \cdot (E / E_{}\mathrm{ref}})^{\gamma}
4462
4563
Parameters
4664
----------
47-
energy: number or array-like
65+
energy: Quantity[energy]
4866
energy points to evaluate
49-
flux_normalization: float
67+
flux_normalization: Quantity[m**-2 s**-2 TeV**-1]
5068
Flux normalization
5169
spectral_index: float
5270
Spectral index
71+
e_ref: Quantity[energy]
72+
The reference energy
5373
'''
54-
return flux_normalization * energy**(-spectral_index)
74+
return flux_normalization * (energy / e_ref)**(spectral_index)
5575

5676

57-
def curved_power_law(energy, flux_normalization, a, b):
77+
@u.quantity_input
78+
def curved_power_law(
79+
energy: u.TeV,
80+
flux_normalization: (FLUX_UNIT, POINT_SOURCE_FLUX_UNIT),
81+
a,
82+
b,
83+
e_ref: u.TeV=1 * u.TeV,
84+
):
5885
r'''
5986
Curved power law
6087
6188
.. math::
62-
\phi = \phi_0 \cdot E ^ {a - b \cdot \log(E)}
89+
\phi = \phi_0 \cdot E ^ {a + b \cdot \log_{10}(E)}
6390
6491
Parameters
6592
----------
@@ -70,9 +97,12 @@ def curved_power_law(energy, flux_normalization, a, b):
7097
a: float
7198
Parameter `a` of the curved power law
7299
b: float
73-
Parameter `b` of the curved power law
100+
Parameter `b` of the curved power law
101+
e_ref: Quantity[energy]
102+
The reference energy
74103
'''
75-
return flux_normalization * energy ** (a + b * np.log10(energy))
104+
exp = a + b * np.log10((energy / e_ref))
105+
return flux_normalization * (energy / e_ref) ** exp
76106

77107

78108
def li_ma_significance(n_on, n_off, alpha=0.2):
@@ -115,7 +145,14 @@ def li_ma_significance(n_on, n_off, alpha=0.2):
115145
return significance
116146

117147

118-
def calc_weight_simple_to_curved(energy, spectral_index, a, b):
148+
@u.quantity_input
149+
def calc_weight_simple_to_curved(
150+
energy: u.TeV,
151+
spectral_index,
152+
a,
153+
b,
154+
e_ref: u.TeV = 1 * u.TeV,
155+
):
119156
'''
120157
Reweight simulated events from a simulated power law with
121158
spectral index `spectral_index` to a curved power law with parameters `a` and `b`
@@ -131,10 +168,16 @@ def calc_weight_simple_to_curved(energy, spectral_index, a, b):
131168
b: float
132169
Parameter `b` of the target curved power law
133170
'''
134-
return energy ** (spectral_index + a + b * np.log10(energy))
171+
return (energy / e_ref) ** (spectral_index + a + b * np.log10(energy / e_ref))
135172

136173

137-
def calc_weight_change_index(energy, simulated_index, target_index):
174+
@u.quantity_input
175+
def calc_weight_change_index(
176+
energy: u.TeV,
177+
simulated_index,
178+
target_index,
179+
e_ref: u.TeV=1 * u.TeV,
180+
):
138181
'''
139182
Reweight simulated events from one power law index to another
140183
@@ -147,60 +190,109 @@ def calc_weight_change_index(energy, simulated_index, target_index):
147190
target_index: float
148191
Spectral index of the target power law
149192
'''
150-
return energy ** (target_index - simulated_index)
193+
return (energy / e_ref) ** (target_index - simulated_index)
194+
195+
196+
@u.quantity_input
197+
def calc_gamma_obstime(
198+
n_events,
199+
spectral_index,
200+
flux_normalization: (FLUX_UNIT, POINT_SOURCE_FLUX_UNIT),
201+
max_impact: u.m,
202+
e_min: u.TeV,
203+
e_max: u.TeV,
204+
e_ref: u.TeV = 1 * u.TeV,
205+
) -> u.s:
206+
r'''
207+
Calculate the equivalent observation time for a number of simulation enents
151208
209+
The number of events produced by sampling from a power law with
210+
spectral index γ is given by
152211
153-
def calc_gamma_obstime(n_events, spectral_index, flux_normalization, max_impact, e_min, e_max):
154-
'''
155-
Calculate the equivalent observation time for a spectral_index montecarlo set
212+
.. math::
213+
N =
214+
A t \Phi_0 \int_{E_{\min}}^{E_{\max}}
215+
\left(\frac{E}{E_{\mathrm{ref}}}\right)^{\gamma}
216+
\,\mathrm{d}E
217+
218+
Solving this for t yields
219+
220+
.. math::
221+
t = \frac{N \cdot (\gamma - 1)}{
222+
E_{\mathrm{ref}} A \Phi_0
223+
\left(
224+
\left(\frac{E_{\max}}{E_{\mathrm{ref}}}\right)^{\gamma - 1}
225+
- \left(\frac{E_{\max}}{E_{\mathrm{ref}}}\right)^{\gamma - 1}
226+
\right)
227+
}
156228
157229
Parameters
158230
----------
159231
n_events: int
160232
Number of simulated events
161233
spectral_index: float
162-
Spectral index of the simulated power law
234+
Spectral index of the simulated power law, including the sign,
235+
so typically -2.7 or -2
163236
flux_normalization: float
164237
Flux normalization of the simulated power law
165-
max_impact: float
238+
max_impact: Quantity[length]
166239
Maximal simulated impact
167-
e_min: float
240+
e_min: Quantity[energy]
168241
Mimimal simulated energy
169-
e_max: float
242+
e_max: Quantity[energy]
170243
Maximal simulated energy
244+
e_ref: Quantity[energy]
245+
The e_ref energy
171246
'''
172247
if spectral_index >= -1:
173248
raise ValueError('spectral_index must be < -1')
174249

175-
numerator = n_events * (1 - spectral_index)
250+
numerator = n_events * (spectral_index + 1)
176251

177-
t1 = flux_normalization * max_impact**2 * np.pi
178-
t2 = e_max**(1 - spectral_index) - e_min**(1 - spectral_index)
252+
A = max_impact**2 * np.pi
253+
t1 = A * flux_normalization * e_ref
254+
t2 = (e_max / e_ref)**(spectral_index + 1)
255+
t3 = (e_min / e_ref)**(spectral_index + 1)
179256

180-
denominator = t1 * t2
257+
denominator = t1 * (t2 - t3)
181258

182259
return numerator / denominator
183260

184261

185-
def power_law_integral(flux_normalisation, spectral_index, e_min, e_max):
262+
@u.quantity_input
263+
def power_law_integral(
264+
flux_normalization: (FLUX_UNIT, POINT_SOURCE_FLUX_UNIT),
265+
spectral_index,
266+
e_min: u.TeV,
267+
e_max: u.TeV,
268+
e_ref: u.TeV = 1 * u.TeV,
269+
):
186270
'''
187271
Return the integral of a power_law with normalisation
188272
`flux_normalization` and index `spectral_index`
189273
between `e_min` and `e_max`
190274
'''
191-
int_index = 1 - spectral_index
192-
return flux_normalisation / int_index * (e_max**(int_index) - e_min**(int_index))
275+
int_index = spectral_index + 1
276+
e_term = (e_max / e_ref)**(int_index) - (e_min / e_ref)**(int_index)
193277

278+
res = flux_normalization * e_ref / int_index * e_term
194279

280+
if flux_normalization.unit.is_equivalent(FLUX_UNIT):
281+
return res.to(1 / u.m**2 / u.s / u.sr)
282+
return res.to(1 / u.m**2 / u.s)
283+
284+
285+
@u.quantity_input
195286
def calc_proton_obstime(
196-
n_events,
197-
spectral_index,
198-
max_impact,
199-
viewcone,
200-
e_min,
201-
e_max,
202-
flux_normalization=1.8e4,
203-
):
287+
n_events,
288+
spectral_index,
289+
max_impact: u.m,
290+
viewcone: u.deg,
291+
e_min: u.TeV,
292+
e_max: u.TeV,
293+
flux_normalization: FLUX_UNIT = 1.8e4 * FLUX_UNIT,
294+
e_ref: u.GeV=1 * u.GeV,
295+
) -> u.s:
204296
'''
205297
Calculate the equivalent observation time for a proton montecarlo set
206298
@@ -214,18 +306,20 @@ def calc_proton_obstime(
214306
Maximal simulated impact in m
215307
viewcone: float
216308
Viewcone in degrees
217-
e_min: float
218-
Mimimal simulated energy in GeV
219-
e_max: float
220-
Maximal simulated energy in GeV
309+
e_min: Quantity[energy]
310+
Mimimal simulated energy
311+
e_max: Quantity[energy]
312+
Maximal simulated energy
221313
flux_normalization: float
222314
Flux normalisation of the cosmic rays in nucleons / (m² s sr GeV)
223315
Default value is (29.2) of
224316
http://pdg.lbl.gov/2016/reviews/rpp2016-rev-cosmic-rays.pdf
225317
'''
226318
area = np.pi * max_impact**2
227-
solid_angle = 2 * np.pi * (1 - np.cos(np.deg2rad(viewcone)))
319+
solid_angle = 2 * np.pi * (1 - np.cos(viewcone)) * u.sr
228320

229-
expected_integral_flux = power_law_integral(flux_normalization, 2.7, e_min, e_max)
321+
expected_integral_flux = power_law_integral(
322+
flux_normalization, spectral_index, e_min, e_max, e_ref
323+
)
230324

231325
return n_events / area / solid_angle / expected_integral_flux

fact/io.py

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def read_h5py(
149149

150150
if 'index' in df.columns:
151151
df.set_index('index', inplace=True)
152+
df.index.name = None
152153

153154
return df
154155

fact/time.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def datetime_to_mjd(dt):
5555

5656
elif isinstance(dt, (pd.Series, pd.DatetimeIndex)):
5757
if dt.tz is None:
58-
dt.tz = timezone.utc
58+
dt = dt.tz_localize(timezone.utc)
5959

6060
return (dt - MJD_EPOCH).total_seconds() / 24 / 3600
6161

tests/test_analysis.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1+
import astropy.units as u
2+
from pytest import approx, raises
3+
4+
15
def test_proton_obstime():
26
from fact.analysis.statistics import calc_proton_obstime
37
n_simulated = 780046520
48
t = calc_proton_obstime(
59
n_events=n_simulated,
6-
spectral_index=2.7,
7-
max_impact=400,
8-
viewcone=5,
9-
e_min=100,
10-
e_max=200e3,
10+
spectral_index=-2.7,
11+
max_impact=400 * u.m,
12+
viewcone=5 * u.deg,
13+
e_min=100 * u.GeV,
14+
e_max=200 * u.TeV,
1115
)
12-
assert int(t) == 15397
16+
assert t.to(u.s).value == approx(15397.82)
17+
18+
19+
def test_power():
20+
from fact.analysis.statistics import random_power
21+
22+
a = random_power(-2.7, e_min=5 * u.GeV, e_max=10 * u.TeV, size=1000)
23+
24+
assert a.shape == (1000, )
25+
assert a.unit == u.TeV
26+
27+
with raises(ValueError):
28+
random_power(2.7, 5 * u.GeV, 10 * u.GeV, e_ref=1 * u.GeV, size=1)

0 commit comments

Comments
 (0)