Passed
Pull Request — master (#1918)
by Christoph
05:37
created

gammapy/astro/population/spatial.py (26 issues)

1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
"""Galactic radial source distribution probability density functions."""
3
from __future__ import absolute_import, division, print_function, unicode_literals
4
import numpy as np
5
from astropy.units import Quantity
6
from astropy.modeling import Fittable1DModel, Parameter
7
from ...utils.coordinates import cartesian, polar, D_SUN_TO_GALACTIC_CENTER
8
from ...utils.random import get_random_state
9
10
__all__ = [
11
    "CaseBattacharya1998",
12
    "FaucherKaspi2006",
13
    "Lorimer2006",
14
    "Paczynski1990",
15
    "YusifovKucuk2004",
16
    "YusifovKucuk2004B",
17
    "Exponential",
18
    "LogSpiral",
19
    "FaucherSpiral",
20
    "ValleeSpiral",
21
    "radial_distributions",
22
]
23
24
# Simulation range used for random number drawing
25
RMIN, RMAX = Quantity([0, 20], "kpc")
26
ZMIN, ZMAX = Quantity([-0.5, 0.5], "kpc")
27
28
29
class Paczynski1990(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
30
    """Radial distribution of the birth surface density of neutron stars - Paczynski 1990.
31
32
    .. math ::
33
        f(r) = A r_{exp}^{-2} \\exp \\left(-\\frac{r}{r_{exp}} \\right)
34
35
    Reference: http://adsabs.harvard.edu/abs/1990ApJ...348..485P (Formula (2))
36
37
    Parameters
38
    ----------
39
    amplitude : float
40
        See formula
41
    r_exp : float
42
        See formula
43
44
    See Also
45
    --------
46
    CaseBattacharya1998, YusifovKucuk2004, Lorimer2006, YusifovKucuk2004B,
47
    FaucherKaspi2006, Exponential
48
    """
49
50
    amplitude = Parameter()
51
    r_exp = Parameter()
52
    evolved = False
53
54
    def __init__(self, amplitude=1, r_exp=4.5, **kwargs):
55
        super(Paczynski1990, self).__init__(amplitude=amplitude, r_exp=r_exp, **kwargs)
56
57
    @staticmethod
58
    def evaluate(r, amplitude, r_exp):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
59
        """Evaluate model."""
60
        return amplitude * r_exp ** -2 * np.exp(-r / r_exp)
61
62
63 View Code Duplication
class CaseBattacharya1998(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
64
    """Radial distribution of the surface density of supernova remnants in the galaxy - Case & Battacharya 1998.
65
66
    .. math ::
67
        f(r) = A \\left( \\frac{r}{r_{\\odot}} \\right) ^ \\alpha \\exp
68
        \\left[ -\\beta \\left( \\frac{ r - r_{\\odot}}{r_{\\odot}} \\right) \\right]
69
70
    Reference: http://adsabs.harvard.edu//abs/1998ApJ...504..761C (Formula (14))
71
72
    Parameters
73
    ----------
74
    amplitude : float
75
        See model formula
76
    alpha : float
77
        See model formula
78
    beta : float
79
        See model formula
80
81
    See Also
82
    --------
83
    Paczynski1990, YusifovKucuk2004, Lorimer2006, YusifovKucuk2004B,
84
    FaucherKaspi2006, Exponential
85
    """
86
87
    amplitude = Parameter()
88
    alpha = Parameter()
89
    beta = Parameter()
90
    evolved = True
91
92
    def __init__(self, amplitude=1.0, alpha=2, beta=3.53, **kwargs):
93
        super(CaseBattacharya1998, self).__init__(
94
            amplitude=amplitude, alpha=alpha, beta=beta, **kwargs
95
        )
96
97
    @staticmethod
98
    def evaluate(r, amplitude, alpha, beta):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
99
        """Evaluate model."""
100
        d_sun = D_SUN_TO_GALACTIC_CENTER.value
101
        term1 = (r / d_sun) ** alpha
102
        term2 = np.exp(-beta * (r - d_sun) / d_sun)
103
        return amplitude * term1 * term2
104
105
106
class YusifovKucuk2004(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
107
    """Radial distribution of the surface density of pulsars in the galaxy - Yusifov & Kucuk 2004.
108
109
    .. math ::
110
        f(r) = A \\left ( \\frac{r + r_1}{r_{\\odot} + r_1} \\right )^a \\exp
111
        \\left [-b \\left( \\frac{r - r_{\\odot}}{r_{\\odot} + r_1} \\right ) \\right ]
112
113
    Used by Faucher-Guigere and Kaspi. Density at ``r = 0`` is nonzero.
114
115
    Reference: http://adsabs.harvard.edu/abs/2004A%26A...422..545Y (Formula (15))
116
117
    Parameters
118
    ----------
119
    amplitude : float
120
        See model formula
121
    a : float
122
        See model formula
123
    b : float
124
        See model formula
125
    r_1 : float
126
        See model formula
127
128
    See Also
129
    --------
130
    CaseBattacharya1998, Paczynski1990, Lorimer2006, YusifovKucuk2004B,
131
    FaucherKaspi2006, Exponential
132
    """
133
134
    amplitude = Parameter()
135
    a = Parameter()
136
    b = Parameter()
137
    r_1 = Parameter()
138
    evolved = True
139
140
    def __init__(self, amplitude=1, a=1.64, b=4.01, r_1=0.55, **kwargs):
141
        super(YusifovKucuk2004, self).__init__(
142
            amplitude=amplitude, a=a, b=b, r_1=r_1, **kwargs
143
        )
144
145
    @staticmethod
146
    def evaluate(r, amplitude, a, b, r_1):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
147
        """Evaluate model."""
148
        d_sun = D_SUN_TO_GALACTIC_CENTER.value
149
        term1 = ((r + r_1) / (d_sun + r_1)) ** a
150
        term2 = np.exp(-b * (r - d_sun) / (d_sun + r_1))
151
        return amplitude * term1 * term2
152
153
154
class YusifovKucuk2004B(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
155
    """Radial distribution of the surface density of OB stars in the galaxy - Yusifov & Kucuk 2004.
156
157
    .. math ::
158
        f(r) = A \\left( \\frac{r}{r_{\\odot}} \\right) ^ a
159
        \\exp \\left[ -b \\left( \\frac{r}{r_{\\odot}} \\right) \\right]
160
161
    Derived empirically from OB-stars distribution.
162
163
    Reference: http://adsabs.harvard.edu/abs/2004A%26A...422..545Y (Formula (17))
164
165
    Parameters
166
    ----------
167
    amplitude : float
168
        See model formula
169
    a : float
170
        See model formula
171
    b : float
172
        See model formula
173
174
    See Also
175
    --------
176
    CaseBattacharya1998, Paczynski1990, YusifovKucuk2004, Lorimer2006,
177
    FaucherKaspi2006, Exponential
178
    """
179
180
    amplitude = Parameter()
181
    a = Parameter()
182
    b = Parameter()
183
    evolved = False
184
185
    def __init__(self, amplitude=1, a=4, b=6.8, **kwargs):
186
        super(YusifovKucuk2004B, self).__init__(amplitude=amplitude, a=a, b=b, **kwargs)
187
188
    @staticmethod
189
    def evaluate(r, amplitude, a, b):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
190
        """Evaluate model."""
191
        d_sun = D_SUN_TO_GALACTIC_CENTER.value
192
        return amplitude * (r / d_sun) ** a * np.exp(-b * (r / d_sun))
193
194
195
class FaucherKaspi2006(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
196
    """Radial distribution of the birth surface density of pulsars in the galaxy - Faucher-Giguere & Kaspi 2006.
0 ignored issues
show
A suspicious escape sequence \p was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
197
198
    .. math ::
199
        f(r) = A \\frac{1}{\\sqrt{2 \pi} \sigma} \\exp
200
        \\left(- \\frac{(r - r_0)^2}{2 \sigma ^ 2}\\right)
201
202
    Reference: http://adsabs.harvard.edu/abs/2006ApJ...643..332F (Appendix B)
203
204
    Parameters
205
    ----------
206
    amplitude : float
207
        See model formula
208
    r_0 : float
209
        See model formula
210
    sigma : float
211
        See model formula
212
213
    See Also
214
    --------
215
    CaseBattacharya1998, Paczynski1990, YusifovKucuk2004, Lorimer2006,
216
    YusifovKucuk2004B, Exponential
217
    """
218
219
    amplitude = Parameter()
220
    r_0 = Parameter()
221
    sigma = Parameter()
222
    evolved = False
223
224
    def __init__(self, amplitude=1, r_0=7.04, sigma=1.83, **kwargs):
225
        super(FaucherKaspi2006, self).__init__(
226
            amplitude=amplitude, r_0=r_0, sigma=sigma, **kwargs
227
        )
228
229
    @staticmethod
230
    def evaluate(r, amplitude, r_0, sigma):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
231
        """Evaluate model."""
232
        term1 = 1.0 / np.sqrt(2 * np.pi * sigma)
233
        term2 = np.exp(-(r - r_0) ** 2 / (2 * sigma ** 2))
234
        return amplitude * term1 * term2
235
236
237 View Code Duplication
class Lorimer2006(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
238
    """Radial distribution of the surface density of pulsars in the galaxy - Lorimer 2006.
239
240
    .. math ::
241
        f(r) = A \\left( \\frac{r}{r_{\\odot}} \\right) ^ B \\exp
242
        \\left[ -C \\left( \\frac{r - r_{\\odot}}{r_{\\odot}} \\right) \\right]
243
244
    Reference: http://adsabs.harvard.edu/abs/2006MNRAS.372..777L (Formula (10))
245
246
    Parameters
247
    ----------
248
    amplitude : float
249
        See model formula
250
    B : float
251
        See model formula
252
    C : float
253
        See model formula
254
255
    See Also
256
    --------
257
    CaseBattacharya1998, Paczynski1990, YusifovKucuk2004, Lorimer2006,
258
    YusifovKucuk2004B, FaucherKaspi2006
259
    """
260
261
    amplitude = Parameter()
262
    B = Parameter()
263
    C = Parameter()
264
    evolved = True
265
266
    def __init__(self, amplitude=1, B=1.9, C=5.0, **kwargs):
267
        super(Lorimer2006, self).__init__(amplitude=amplitude, B=B, C=C, **kwargs)
268
269
    @staticmethod
270
    def evaluate(r, amplitude, B, C):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
271
        """Evaluate model."""
272
        d_sun = D_SUN_TO_GALACTIC_CENTER.value
273
        term1 = (r / d_sun) ** B
274
        term2 = np.exp(-C * (r - d_sun) / d_sun)
275
        return amplitude * term1 * term2
276
277
278
class Exponential(Fittable1DModel):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
279
    """Exponential distribution.
280
281
    .. math ::
282
        f(z) = A \\exp \\left(- \\frac{|z|}{z_0} \\right)
283
284
    Usually used for height distribution above the Galactic plane,
285
    with 0.05 kpc as a commonly used birth height distribution.
286
287
    Parameters
288
    ----------
289
    amplitude : float
290
        See model formula
291
    z_0 : float
292
        Scale height of the distribution
293
294
    See Also
295
    --------
296
    CaseBattacharya1998, Paczynski1990, YusifovKucuk2004, Lorimer2006,
297
    YusifovKucuk2004B, FaucherKaspi2006, Exponential
298
    """
299
300
    amplitude = Parameter()
301
    z_0 = Parameter()
302
    evolved = False
303
304
    def __init__(self, amplitude=1, z_0=0.05, **kwargs):
305
        super(Exponential, self).__init__(amplitude=amplitude, z_0=z_0, **kwargs)
306
307
    @staticmethod
308
    def evaluate(z, amplitude, z_0):
0 ignored issues
show
Parameters differ from overridden 'evaluate' method
Loading history...
309
        """Evaluate model."""
310
        return amplitude * np.exp(-np.abs(z) / z_0)
311
312
313
class LogSpiral(object):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
314
    """Logarithmic spiral.
315
316
    Reference: http://en.wikipedia.org/wiki/Logarithmic_spiral
317
    """
318
319
    def xy_position(self, theta=None, radius=None, spiralarm_index=0):
320
        """Compute (x, y) position for a given angle or radius.
321
322
        Parameters
323
        ----------
324
        theta : array_like
325
            Angle (deg)
326
        radius : array_like
327
            Radius (kpc)
328
        spiralarm_index : int
329
            Spiral arm index
330
331
        Returns
332
        -------
333
        x, y : array_like
334
            Position (x, y)
335
        """
336
        if (theta is None) and not (radius is None):
337
            theta = self.theta(radius, spiralarm_index=spiralarm_index)
338
        elif (radius is None) and not (theta is None):
339
            radius = self.radius(theta, spiralarm_index=spiralarm_index)
340
        else:
341
            raise ValueError("Specify only one of: theta, radius")
342
343
        theta = np.radians(theta)
344
        x = radius * np.cos(theta)
345
        y = radius * np.sin(theta)
346
        return x, y
347
348
    def radius(self, theta, spiralarm_index):
349
        """Radius for a given angle.
350
351
        Parameters
352
        ----------
353
        theta : array_like
354
            Angle (deg)
355
        spiralarm_index : int
356
            Spiral arm index
357
358
        Returns
359
        -------
360
        radius : array_like
361
            Radius (kpc)
362
        """
363
        k = self.k[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named k.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
364
        r_0 = self.r_0[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named r_0.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
365
        theta_0 = self.theta_0[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named theta_0.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
366
        d_theta = np.radians(theta - theta_0)
367
        radius = r_0 * np.exp(d_theta / k)
368
        return radius
369
370
    def theta(self, radius, spiralarm_index):
371
        """Angle for a given radius.
372
373
        Parameters
374
        ----------
375
        radius : array_like
376
            Radius (kpc)
377
        spiralarm_index : int
378
            Spiral arm index
379
380
        Returns
381
        -------
382
        theta : array_like
383
            Angle (deg)
384
        """
385
        k = self.k[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named k.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
386
        r_0 = self.r_0[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named r_0.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
387
        theta_0 = self.theta_0[spiralarm_index]
0 ignored issues
show
The Instance of LogSpiral does not seem to have a member named theta_0.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
388
        theta_0 = np.radians(theta_0)
389
        theta = k * np.log(radius / r_0) + theta_0
390
        return np.degrees(theta)
391
392
393
class FaucherSpiral(LogSpiral):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
394
    """Milky way spiral arm used in Faucher et al (2006).
395
396
    Reference: http://adsabs.harvard.edu/abs/2006ApJ...643..332F
397
    """
398
399
    # Parameters
400
    k = Quantity([4.25, 4.25, 4.89, 4.89], "rad")
401
    r_0 = Quantity([3.48, 3.48, 4.9, 4.9], "kpc")
402
    theta_0 = Quantity([1.57, 4.71, 4.09, 0.95], "rad")
403
    spiralarms = np.array(["Norma", "Carina Sagittarius", "Perseus", "Crux Scutum"])
404
405
    def _blur(self, radius, theta, amount=0.07, random_state="random-seed"):
406
        """Blur the positions around the centroid of the spiralarm.
407
408
        The given positions are blurred by drawing a displacement in radius from
409
        a normal distribution, with sigma = amount * radius. And a direction
410
        theta from a uniform distribution in the interval [0, 2 * pi].
411
412
        Parameters
413
        ----------
414
        radius : `~astropy.units.Quantity`
415
            Radius coordinate
416
        theta : `~astropy.units.Quantity`
417
            Angle coordinate
418
        amount: float, optional
419
            Amount of blurring of the position, given as a fraction of `radius`.
420
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
421
            Defines random number generator initialisation.
422
            Passed to `~gammapy.utils.random.get_random_state`.
423
        """
424
        random_state = get_random_state(random_state)
425
426
        dr = Quantity(abs(random_state.normal(0, amount * radius, radius.size)), "kpc")
427
        dtheta = Quantity(random_state.uniform(0, 2 * np.pi, radius.size), "rad")
428
        x, y = cartesian(radius, theta)
429
        dx, dy = cartesian(dr, dtheta)
430
        return polar(x + dx, y + dy)
431
432
    def _gc_correction(
433
        self, radius, theta, r_corr=Quantity(2.857, "kpc"), random_state="random-seed"
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
434
    ):
435
        """Correction of source distribution towards the galactic center.
436
437
        To avoid spiralarm features near the Galactic Center, the position angle theta
438
        is blurred by a certain amount towards the GC.
439
440
        Parameters
441
        ----------
442
        radius : `~astropy.units.Quantity`
443
            Radius coordinate
444
        theta : `~astropy.units.Quantity`
445
            Angle coordinate
446
        r_corr : `~astropy.units.Quantity`, optional
447
            Scale of the correction towards the GC
448
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
449
            Defines random number generator initialisation.
450
            Passed to `~gammapy.utils.random.get_random_state`.
451
        """
452
        random_state = get_random_state(random_state)
453
454
        theta_corr = Quantity(random_state.uniform(0, 2 * np.pi, radius.size), "rad")
455
        return radius, theta + theta_corr * np.exp(-radius / r_corr)
456
457
    def __call__(self, radius, blur=True, random_state="random-seed"):
458
        """Draw random position from spiral arm distribution.
459
460
        Returns the corresponding angle theta[rad] to a given radius[kpc] and number of spiralarm.
461
        Possible numbers are:
462
463
        * Norma = 0,
464
        * Carina Sagittarius = 1,
465
        * Perseus = 2
466
        * Crux Scutum = 3.
467
468
        Parameters
469
        ----------
470
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
471
            Defines random number generator initialisation.
472
            Passed to `~gammapy.utils.random.get_random_state`.
473
474
        Returns
475
        -------
476
        Returns dx and dy, if blurring= true.
477
        """
478
        random_state = get_random_state(random_state)
479
480
        # Choose spiral arm
481
        N = random_state.randint(0, 4, radius.size)
482
        theta = self.k[N] * np.log(radius / self.r_0[N]) + self.theta_0[N]
483
        spiralarm = self.spiralarms[N]
484
485
        if blur:  # Apply blurring model according to Faucher
486
            radius, theta = self._blur(radius, theta, random_state=random_state)
487
            radius, theta = self._gc_correction(
488
                radius, theta, random_state=random_state
489
            )
490
        return radius, theta, spiralarm
491
492
493
class ValleeSpiral(LogSpiral):
0 ignored issues
show
The variable __class__ seems to be unused.
Loading history...
494
    """Milky way spiral arm model from Vallee (2008).
495
496
    Reference: http://adsabs.harvard.edu/abs/2008AJ....135.1301V
497
    """
498
499
    # Model parameters
500
    p = Quantity(12.8, "deg")  # pitch angle in deg
501
    m = 4  # number of spiral arms
502
    r_sun = Quantity(7.6, "kpc")  # distance sun to Galactic center in kpc
503
    r_0 = Quantity(2.1, "kpc")  # spiral inner radius in kpc
504
    theta_0 = Quantity(-20, "deg")  # Norma spiral arm start angle
505
    bar_radius = Quantity(3.0, "kpc")  # Radius of the galactic bar (not equal r_0!)
506
507
    spiralarms = np.array(["Norma", "Perseus", "Carina Sagittarius", "Crux Scutum"])
508
509
    def __init__(self):
510
        self.r_0 = self.r_0 * np.ones(4)
511
        self.theta_0 = self.theta_0 + Quantity([0, 90, 180, 270], "deg")
512
        self.k = Quantity(1.0 / np.tan(np.radians(self.p.value)) * np.ones(4), "rad")
513
514
        # Compute start and end point of the bar
515
        x_0, y_0 = self.xy_position(radius=self.bar_radius, spiralarm_index=0)
516
        x_1, y_1 = self.xy_position(radius=self.bar_radius, spiralarm_index=2)
517
        self.bar = dict(x=Quantity([x_0, x_1]), y=Quantity([y_0, y_1]))
518
519
520
"""Radial distribution (dict mapping names to classes)."""
521
radial_distributions = {
522
    "CB98": CaseBattacharya1998,
523
    "F06": FaucherKaspi2006,
524
    "L06": Lorimer2006,
525
    "P90": Paczynski1990,
526
    "YK04": YusifovKucuk2004,
527
    "YK04B": YusifovKucuk2004B,
528
}
529