Skip to content

Commit 7ada2a5

Browse files
authored
Merge pull request #54 from lenasaurenhaus/master
Power law spectrum with subexponential cutoff
2 parents 45c1c9c + b988b80 commit 7ada2a5

File tree

4 files changed

+178
-11
lines changed

4 files changed

+178
-11
lines changed

icecube_tools/source/flux_model.py

+89
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .power_law import BoundedPowerLaw
66
from .power_law import BrokenBoundedPowerLaw
77
from .power_law import BoundedPowerLawExpCutoff
8+
from .power_law import BoundedPowerLawSubexpCutoff
89

910
"""
1011
Module for simple flux models used in
@@ -477,3 +478,91 @@ def sample(self, N):
477478
:param N: Number of samples.
478479
"""
479480
return self.power_law.samples(N)
481+
482+
483+
class PowerLawSubexpCutoffFlux(FluxModel):
484+
"""
485+
Power law flux models with a subexponential cutoff.
486+
"""
487+
488+
def __init__(
489+
self,
490+
normalisation,
491+
norm_energy,
492+
index1,
493+
cutoff_energy,
494+
index2,
495+
lower_energy=1e2,
496+
upper_energy=1e8
497+
):
498+
"""
499+
Power law flux models with a subexponential cutoff.
500+
501+
:param normalisation: Flux normalisation [GeV^-1 cm^-2 s^-1 sr^-1] or [GeV^-1 cm^-2 s^-1] for point sources.
502+
:param norm_energy: Energy at which the spectrum is normalised [GeV].
503+
:param index1: Spectral index of the power law.
504+
:param cutoff_energy: Cutoff energy [GeV].
505+
:param index2: Spectral index in the exponential function. 0 < index2 < 1.
506+
:param lower_energy: Lower energy bound [GeV].
507+
:param upper_energy: Upper energy bound [GeV].
508+
"""
509+
510+
super().__init__()
511+
512+
self._normalisation = normalisation
513+
514+
self._norm_energy = norm_energy
515+
516+
self._index1 = index1
517+
518+
self._cutoff_energy = cutoff_energy
519+
520+
self._index2 = index2
521+
522+
self._lower_energy = lower_energy
523+
524+
self._upper_energy = upper_energy
525+
526+
self.power_law = BoundedPowerLawSubexpCutoff(self._index1, self._cutoff_energy, self._index2, self._lower_energy, self._upper_energy)
527+
528+
def spectrum(self, energy):
529+
"""
530+
dN/dEdAdt or dN/dEdAdtdO.
531+
"""
532+
output = self._normalisation * (energy/self._norm_energy)**(-self._index1) * np.exp(-1 * (energy/self._cutoff_energy)**self._index2)
533+
534+
if isinstance(energy, np.ndarray):
535+
idx = np.logical_or(energy < self._lower_energy, energy > self._upper_energy)
536+
output[idx] = np.zeros(len(output[idx]))
537+
return output
538+
539+
else:
540+
if energy < self._lower_energy or energy > self._upper_energy:
541+
return 0.0
542+
else:
543+
return output
544+
545+
def integrated_spectrum(self, lower_energy_bound, upper_energy_bound):
546+
"""
547+
Integrates the spectrum with respect to E over a finite energy interval.
548+
:param lower_energy_bound: in GeV
549+
:param upper_energy_bound: in GeV
550+
"""
551+
norm = self._normalisation
552+
E0 = self._norm_energy
553+
Ecut = self._cutoff_energy
554+
gamma = self._index1
555+
lambda_ = self._index2
556+
557+
E1 = lower_energy_bound
558+
E2 = upper_energy_bound
559+
incGamma = np.frompyfunc(mp.gammainc, 3, 1)
560+
561+
if isinstance(lower_energy_bound, np.ndarray) or isinstance(upper_energy_bound, np.ndarray):
562+
return norm/E0**(-gamma) * Ecut**(1-gamma)/lambda_ * incGamma((1-gamma)/lambda_, (E1/Ecut)**lambda_, (E2/Ecut)**lambda_).astype('float64')
563+
564+
else:
565+
return norm/E0**(-gamma) * Ecut**(1-gamma)/lambda_ * float(incGamma((1-gamma)/lambda_, (E1/Ecut)**lambda_, (E2/Ecut)**lambda_))
566+
567+
def redshift_factor(self, z: float):
568+
return 1.0

icecube_tools/source/power_law.py

+71-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def cdf(self, x):
248248
if x < self.xmin:
249249
return 0.0
250250
elif x > self.xmax:
251-
return 0.0
251+
return 1.0
252252
else:
253253
return val
254254

@@ -278,3 +278,73 @@ def samples(self, nsamples):
278278
"""
279279
u = np.random.uniform(0, 1, size=nsamples)
280280
return self.inv_cdf(u)
281+
282+
283+
class BoundedPowerLawSubexpCutoff:
284+
"""
285+
Definition of a bounded power law with a subexponential cutoff.
286+
"""
287+
288+
def __init__(self, gamma, xcut, lambda_, xmin, xmax):
289+
"""
290+
Definition of a bounded power law with a subexponential cutoff.
291+
"""
292+
self.gamma = gamma
293+
self.xcut = xcut
294+
self.lambda_ = lambda_
295+
self.xmin = xmin
296+
self.xmax = xmax
297+
298+
# Calculate the normalisation.
299+
self.norm = 1. / float(
300+
self.xcut ** (1 - self.gamma) / self.lambda_
301+
* mp.gammainc((1 - self.gamma) / self.lambda_,
302+
(self.xmin / self.xcut) ** self.lambda_,
303+
(self.xmax/self.xcut) ** self.lambda_))
304+
305+
def pdf(self, x):
306+
"""
307+
Evaluates the probability density function at x.
308+
"""
309+
val = self.norm * x ** (-self.gamma) * np.exp(-1 * (x / self.xcut) ** self.lambda_)
310+
311+
if not isinstance(x, np.ndarray):
312+
if x < self.xmin or x > self.xmax:
313+
return 0.0
314+
else:
315+
return val
316+
317+
else:
318+
idx = np.logical_or(x < self.xmin, x > self.xmax)
319+
val[idx] = np.zeros(len(val[idx]))
320+
return val
321+
322+
def cdf(self, x):
323+
"""
324+
Evaluated the cumulative distribution function at x.
325+
"""
326+
incGamma = np.frompyfunc(mp.gammainc, 3, 1)
327+
val = self.norm * self.xcut ** (1 - self.gamma) / self.lambda_ * incGamma((1 - self.gamma) / self.lambda_,
328+
(self.xmin / self.xcut) ** self.lambda_,
329+
(x / self.xcut) ** self.lambda_).astype('float64')
330+
331+
if not isinstance(x, np.ndarray):
332+
if x < self.xmin:
333+
return 0.0
334+
elif x > self.xmax:
335+
return 1.0
336+
else:
337+
return val
338+
339+
else:
340+
idx = x < self.xmin
341+
val[idx] = np.zeros(len(val[idx]))
342+
idx = x > self.xmax
343+
val[idx] = np.ones(len(val[idx]))
344+
return val
345+
346+
def inv_cdf(self, x):
347+
raise NotImplementedError
348+
349+
def samples(self, nsamples):
350+
raise NotImplementedError

tests/test_simulation.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from icecube_tools.detector.angular_resolution import AngularResolution
77
from icecube_tools.detector.r2021 import R2021IRF
88
from icecube_tools.detector.detector import IceCube
9-
from icecube_tools.source.flux_model import PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux
9+
from icecube_tools.source.flux_model import PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux, PowerLawSubexpCutoffFlux
1010
from icecube_tools.source.source_model import DiffuseSource, PointSource
1111
from icecube_tools.neutrino_calculator import NeutrinoCalculator, PhiSolver
1212
from icecube_tools.simulator import Simulator, TimeDependentSimulator
@@ -33,12 +33,18 @@
3333
point_source_plec = PointSource(plec_flux, z=0.0)
3434
diffuse_source_plec = DiffuseSource(plec_flux)
3535

36-
sources = [point_source, diffuse_source, point_source_bpl, diffuse_source_bpl, point_source_plec, diffuse_source_plec]
36+
pl_subexp_cutoff_params = (1e-13, 1e3, 2., 1e3, 0.5, 1e4, 1e8)
37+
pl_subexp_cutoff_flux = PowerLawSubexpCutoffFlux(*pl_subexp_cutoff_params)
38+
point_source_pl_subexp_cutoff = PointSource(pl_subexp_cutoff_flux, z=0.0)
39+
diffuse_source_pl_subexp_cutoff = DiffuseSource(pl_subexp_cutoff_flux)
40+
41+
sources = [point_source, diffuse_source, point_source_bpl, diffuse_source_bpl, point_source_plec,
42+
diffuse_source_plec, point_source_pl_subexp_cutoff, diffuse_source_pl_subexp_cutoff]
3743

3844

3945
def test_nu_calc():
4046

41-
nu_calc = NeutrinoCalculator([point_source, point_source_bpl, point_source_plec], aeff)
47+
nu_calc = NeutrinoCalculator([point_source, point_source_bpl, point_source_plec, point_source_pl_subexp_cutoff], aeff)
4248

4349
Nnu = nu_calc(
4450
time=1,

tests/test_source_interface.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import numpy as np
22

3-
from icecube_tools.source.flux_model import PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux
3+
from icecube_tools.source.flux_model import PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux, PowerLawSubexpCutoffFlux
44
from icecube_tools.source.source_model import PointSource, DiffuseSource
55

66

77
pl_params = (1e-18, 1e5, 2.2, 1e4, 1e8)
88
bpl_params = (1e-18, 1e5, 2.2, 3.0, 1e4, 1e8)
99
plec_params = (1e-13, 1e3, -2., 1e3, 1e4, 1e8)
10+
pl_subexp_cutoff_params = (1e-13, 1e3, 2., 1e3, 0.5, 1e4, 1e8)
1011

11-
flux_models = [PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux]
12-
params_list = [pl_params, bpl_params, plec_params]
12+
flux_models = [PowerLawFlux, BrokenPowerLawFlux, PowerLawExpCutoffFlux, PowerLawSubexpCutoffFlux]
13+
params_list = [pl_params, bpl_params, plec_params, pl_subexp_cutoff_params]
1314

1415

1516
def test_flux_models():
@@ -33,7 +34,7 @@ def test_flux_models():
3334

3435
assert int_low > int_high
3536

36-
elif flux_model == PowerLawExpCutoffFlux:
37+
elif flux_model == PowerLawExpCutoffFlux or flux_model == PowerLawSubexpCutoffFlux:
3738
integral = flux.integrated_spectrum(1e4, 1e6)
3839

3940
assert integral > 0
@@ -43,11 +44,12 @@ def test_flux_models():
4344
assert flux.total_flux_density() > 0
4445

4546
# Check sampling
46-
samples = flux.sample(1000)
47+
if not flux_model == PowerLawSubexpCutoffFlux:
48+
samples = flux.sample(1000)
4749

48-
assert np.all(samples >= 1e4)
50+
assert np.all(samples >= 1e4)
4951

50-
assert np.all(samples <= 1e8)
52+
assert np.all(samples <= 1e8)
5153

5254

5355
def test_source_definition():

0 commit comments

Comments
 (0)