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

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

1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
"""Simulate source catalogs."""
3
from __future__ import absolute_import, division, print_function, unicode_literals
4
import numpy as np
5
from ...extern import six
6
from astropy.table import Table, Column
0 ignored issues
show
external import "from astropy.table import Table, Column" should be placed before "from ...extern import six"
Loading history...
7
from astropy.units import Quantity
0 ignored issues
show
external import "from astropy.units import Quantity" should be placed before "from ...extern import six"
Loading history...
8
from astropy.coordinates import SkyCoord, spherical_to_cartesian
0 ignored issues
show
external import "from astropy.coordinates import SkyCoord, spherical_to_cartesian" should be placed before "from ...extern import six"
Loading history...
9
from ...utils import coordinates as astrometry
10
from ...utils.coordinates import D_SUN_TO_GALACTIC_CENTER
11
from ...utils.distributions import draw, pdf
12
from ...utils.random import sample_sphere, sample_sphere_distance, get_random_state
13
from ..source import SNR, SNRTrueloveMcKee, PWN, Pulsar
14
from ..population.spatial import (
15
    Exponential,
16
    FaucherSpiral,
17
    RMIN,
18
    RMAX,
19
    ZMIN,
20
    ZMAX,
21
    radial_distributions,
22
)
23
from ..population.velocity import VMIN, VMAX, velocity_distributions
24
25
__all__ = [
26
    "make_catalog_random_positions_cube",
27
    "make_catalog_random_positions_sphere",
28
    "make_base_catalog_galactic",
29
    "add_snr_parameters",
30
    "add_pulsar_parameters",
31
    "add_pwn_parameters",
32
    "add_observed_source_parameters",
33
    "add_observed_parameters",
34
]
35
36
37
def make_catalog_random_positions_cube(
38
    size=100, dimension=3, dmax=10, random_state="random-seed"
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
39
):
40
    """Make a catalog of sources randomly distributed on a line, square or cube.
41
42
    TODO: is this useful enough for general use or should we hide it as an
43
      internal method to generate test datasets?
44
45
    Parameters
46
    ----------
47
    size : int, optional
48
        Number of sources
49
    dimension : int, optional
50
        Number of dimensions
51
    dmax : int, optional
52
        Maximum distance in pc.
53
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
54
        Defines random number generator initialisation.
55
        Passed to `~gammapy.utils.random.get_random_state`.
56
57
    Returns
58
    -------
59
    catalog : `~astropy.table.Table`
60
        Source catalog with columns:
61
    """
62
    random_state = get_random_state(random_state)
63
64
    # Generate positions 1D, 2D, or 3D
65
    if dimension == 3:
66
        x = random_state.uniform(-dmax, dmax, size)
67
        y = random_state.uniform(-dmax, dmax, size)
68
        z = random_state.uniform(-dmax, dmax, size)
69
    elif dimension == 2:
70
        x = random_state.uniform(-dmax, dmax, size)
71
        y = random_state.uniform(-dmax, dmax, size)
72
        z = np.zeros_like(x)
73
    else:
74
        x = random_state.uniform(-dmax, dmax, size)
75
        y = np.zeros_like(x)
76
        z = np.zeros_like(x)
77
78
    table = Table()
79
    table["x"] = Column(x, unit="pc", description="Galactic cartesian coordinate")
80
    table["y"] = Column(y, unit="pc", description="Galactic cartesian coordinate")
81
    table["z"] = Column(z, unit="pc", description="Galactic cartesian coordinate")
82
83
    return table
84
85
86
def make_catalog_random_positions_sphere(
87
    size, center="Earth", distance=Quantity([0, 1], "Mpc"), random_state="random-seed"
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
88
):
89
    """Sample random source locations in a sphere.
90
91
    This can be used to generate an isotropic source population
92
    to represent extra-galactic sources.
93
94
    Parameters
95
    ----------
96
    size : int
97
        Number of sources
98
    center : {'Earth', 'Milky Way'}
99
        Sphere center
100
    distance : `~astropy.units.Quantity` tuple
101
        Distance min / max range.
102
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
103
        Defines random number generator initialisation.
104
        Passed to `~gammapy.utils.random.get_random_state`.
105
106
    Returns
107
    -------
108
    catalog : `~astropy.table.Table`
109
        Source catalog with columns:
110
111
        - RAJ2000, DEJ2000 (deg)
112
        - GLON, GLAT (deg)
113
        - Distance (Mpc)
114
    """
115
    random_state = get_random_state(random_state)
116
117
    lon, lat = sample_sphere(size, random_state=random_state)
118
    radius = sample_sphere_distance(
119
        distance[0], distance[1], size, random_state=random_state
120
    )
121
122
    # TODO: it shouldn't be necessary here to convert to cartesian ourselves ...
0 ignored issues
show
TODO and FIXME comments should generally be avoided.
Loading history...
123
    x, y, z = spherical_to_cartesian(radius, lat, lon)
124
    pos = SkyCoord(x, y, z, frame="galactocentric", representation="cartesian")
125
126
    if center == "Milky Way":
127
        pass
128
    elif center == "Earth":
129
        # TODO: add shift Galactic center -> Earth
0 ignored issues
show
TODO and FIXME comments should generally be avoided.
Loading history...
130
        raise NotImplementedError
131
    else:
132
        msg = "Invalid center: {}\n".format(center)
133
        msg += "Choose one of: Earth, Milky Way"
134
        raise ValueError(msg)
135
136
    table = Table()
137
    table.meta["center"] = center
138
139
    icrs = pos.transform_to("icrs")
140
    table["RAJ2000"] = icrs.ra.to("deg")
141
    table["DEJ2000"] = icrs.dec.to("deg")
142
143
    galactic = icrs.transform_to("galactic")
144
    table["GLON"] = galactic.l.to("deg")
145
    table["GLAT"] = galactic.b.to("deg")
146
147
    table["Distance"] = icrs.distance.to("Mpc")
148
149
    return table
150
151
152
def make_base_catalog_galactic(
0 ignored issues
show
Too many arguments (7/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (26/15).
Loading history...
153
    n_sources,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
154
    rad_dis="YK04",
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
155
    vel_dis="H05",
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
156
    max_age=Quantity(1e6, "yr"),
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
157
    spiralarms=True,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
158
    n_ISM=Quantity(1, "cm-3"),
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
159
    random_state="random-seed",
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
160
):
161
    """Make a catalog of Galactic sources, with basic source parameters.
162
163
    Choose a radial distribution, a velocity distribution, the number
164
    of pulsars n_pulsars, the maximal age max_age[years] and the fraction
165
    of the individual morphtypes. There's an option spiralarms. If set on
166
    True a spiralarm modelling after Faucher&Kaspi is included.
167
168
    max_age and n_sources effectively correspond to s SN rate:
169
    SN_rate = n_sources / max_age
170
171
    Parameters
172
    ----------
173
    n_sources : int
174
        Number of sources to simulate.
175
    rad_dis : callable
176
        Radial surface density distribution of sources.
177
    vel_dis : callable
178
        Proper motion velocity distribution of sources.
179
    max_age : `~astropy.units.Quantity`
180
        Maximal age of the source
181
    spiralarms : bool
182
        Include a spiralarm model in the catalog.
183
    n_ISM : `~astropy.units.Quantity`
184
        Density of the interstellar medium.
185
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
186
        Defines random number generator initialisation.
187
        Passed to `~gammapy.utils.random.get_random_state`.
188
189
    Returns
190
    -------
191
    table : `~astropy.table.Table`
192
        Catalog of simulated source positions and proper velocities.
193
    """
194
    random_state = get_random_state(random_state)
195
196
    if isinstance(rad_dis, six.string_types):
197
        rad_dis = radial_distributions[rad_dis]
198
199
    if isinstance(vel_dis, six.string_types):
200
        vel_dis = velocity_distributions[vel_dis]
201
202
    # Draw random values for the age
203
    age = random_state.uniform(0, max_age.to_value("yr"), n_sources)
204
    age = Quantity(age, "yr")
205
206
    # Draw r and z values from the given distribution
207
    r = draw(
208
        RMIN.to_value("kpc"),
209
        RMAX.to_value("kpc"),
210
        n_sources,
211
        pdf(rad_dis()),
212
        random_state=random_state,
213
    )
214
    r = Quantity(r, "kpc")
215
216
    z = draw(
217
        ZMIN.to_value("kpc"),
218
        ZMAX.to_value("kpc"),
219
        n_sources,
220
        Exponential(),
221
        random_state=random_state,
222
    )
223
    z = Quantity(z, "kpc")
224
225
    # Apply spiralarm modelling or not
226
    if spiralarms:
227
        r, theta, spiralarm = FaucherSpiral()(r, random_state=random_state)
228
    else:
229
        theta = Quantity(random_state.uniform(0, 2 * np.pi, n_sources), "rad")
230
        spiralarm = None
231
232
    # Compute cartesian coordinates
233
    x, y = astrometry.cartesian(r, theta)
234
235
    # Draw values from velocity distribution
236
    v = draw(
237
        VMIN.to_value("km/s"),
238
        VMAX.to_value("km/s"),
239
        n_sources,
240
        vel_dis(),
241
        random_state=random_state,
242
    )
243
    v = Quantity(v, "km/s")
244
245
    # Draw random direction of initial velocity
246
    theta = Quantity(random_state.uniform(0, np.pi, x.size), "rad")
247
    phi = Quantity(random_state.uniform(0, 2 * np.pi, x.size), "rad")
248
249
    # Compute new position
250
    dx, dy, dz, vx, vy, vz = astrometry.motion_since_birth(v, age, theta, phi)
251
252
    # Add displacement to birth position
253
    x_moved = x + dx
254
    y_moved = y + dy
255
    z_moved = z + dz
256
257
    # Set environment interstellar density
258
    n_ISM = n_ISM * np.ones(n_sources)
259
260
    table = Table()
261
    table["age"] = Column(age, unit="yr", description="Age of the source")
262
    table["n_ISM"] = Column(
263
        n_ISM, unit="cm-3", description="Interstellar medium density"
264
    )
265
    if spiralarms:
266
        table["spiralarm"] = Column(spiralarm, description="Which spiralarm?")
267
268
    table["x_birth"] = Column(
269
        x, unit="kpc", description="Galactocentric x coordinate at birth"
270
    )
271
    table["y_birth"] = Column(
272
        y, unit="kpc", description="Galactocentric y coordinate at birth"
273
    )
274
    table["z_birth"] = Column(
275
        z, unit="kpc", description="Galactocentric z coordinate at birth"
276
    )
277
278
    table["x"] = Column(
279
        x_moved.to("kpc"), unit="kpc", description="Galactocentric x coordinate"
280
    )
281
    table["y"] = Column(
282
        y_moved.to("kpc"), unit="kpc", description="Galactocentric y coordinate"
283
    )
284
    table["z"] = Column(
285
        z_moved.to("kpc"), unit="kpc", description="Galactocentric z coordinate"
286
    )
287
288
    table["vx"] = Column(
289
        vx.to("km/s"), unit="km/s", description="Galactocentric velocity in x direction"
290
    )
291
    table["vy"] = Column(
292
        vy.to("km/s"), unit="km/s", description="Galactocentric velocity in y direction"
293
    )
294
    table["vz"] = Column(
295
        vz.to("km/s"), unit="km/s", description="Galactocentric velocity in z direction"
296
    )
297
    table["v_abs"] = Column(
298
        v, unit="km/s", description="Galactocentric velocity (absolute)"
299
    )
300
301
    return table
302
303
304
def add_snr_parameters(table):
305
    """Add SNR parameters to the table."""
306
    # Read relevant columns
307
    age = table["age"].quantity
308
    n_ISM = table["n_ISM"].quantity
309
310
    # Compute properties
311
    snr = SNR(n_ISM=n_ISM)
312
    E_SN = snr.e_sn * np.ones(len(table))
313
    r_out = snr.radius(age)
314
    r_in = snr.radius_inner(age)
315
    L_SNR = snr.luminosity_tev(age)
316
317
    # Add columns to table
318
    table["E_SN"] = Column(E_SN, unit="erg", description="SNR kinetic energy")
319
    table["r_out"] = Column(r_out, unit="pc", description="SNR outer radius")
320
    table["r_in"] = Column(r_in, unit="pc", description="SNR inner radius")
321
    table["L_SNR"] = Column(L_SNR, unit="s-1", description="SNR luminosity")
322
    return table
323
324
325
def add_pulsar_parameters(
0 ignored issues
show
Too many arguments (6/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (18/15).
Loading history...
326
    table,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
327
    B_mean=12.05,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
328
    B_stdv=0.55,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
329
    P_mean=0.3,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
330
    P_stdv=0.15,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
331
    random_state="random-seed",
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
332
):
333
    """Add pulsar parameters to the table.
334
335
    For the initial normal distribution of period and logB can exist the following
336
    Parameters: B_mean=12.05[log Gauss], B_stdv=0.55, P_mean=0.3[s], P_stdv=0.15
337
338
    Parameters
339
    ----------
340
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
341
        Defines random number generator initialisation.
342
        Passed to `~gammapy.utils.random.get_random_state`.
343
344
    """
345
    random_state = get_random_state(random_state)
346
    # Read relevant columns
347
    age = table["age"].quantity
348
349
    # Draw the initial values for the period and magnetic field
350
    def p_dist(x):
351
        return np.exp(-0.5 * ((x - P_mean) / P_stdv) ** 2)
352
353
    p0_birth = draw(0, 2, len(table), p_dist, random_state=random_state)
354
    p0_birth = Quantity(p0_birth, "s")
355
356
    logB = random_state.normal(B_mean, B_stdv, len(table))
357
358
    # Compute pulsar parameters
359
    psr = Pulsar(p0_birth, logB)
360
    p0 = psr.period(age)
361
    p1 = psr.period_dot(age)
362
    p1_birth = psr.P_dot_0
363
    tau = psr.tau(age)
364
    tau_0 = psr.tau_0
365
    l_psr = psr.luminosity_spindown(age)
366
    l0_psr = psr.L_0
367
368
    # Add columns to table
369
    table["P0"] = Column(p0, unit="s", description="Pulsar period")
370
    table["P1"] = Column(p1, unit="", description="Pulsar period derivative")
371
    table["P0_birth"] = Column(p0_birth, unit="s", description="Pulsar birth period")
372
    table["P1_birth"] = Column(
373
        p1_birth, unit="", description="Pulsar birth period derivative"
374
    )
375
    table["CharAge"] = Column(tau, unit="yr", description="Pulsar characteristic age")
376
    table["Tau0"] = Column(tau_0, unit="yr")
377
    table["L_PSR"] = Column(l_psr, unit="erg s-1")
378
    table["L0_PSR"] = Column(l0_psr, unit="erg s-1")
379
    table["logB"] = Column(logB, unit="Gauss")
380
    return table
381
382
383
def add_pwn_parameters(table):
384
    """Add PWN parameters to the table."""
385
    # Some of the computations (specifically `pwn.radius`) aren't vectorised
386
    # across all parameters; so here we loop over source parameters explicitly
387
388
    results = []
389
390
    for idx in range(len(table)):
391
        age = table["age"].quantity[idx]
392
        E_SN = table["E_SN"].quantity[idx]
393
        n_ISM = table["n_ISM"].quantity[idx]
394
        P0_birth = table["P0_birth"].quantity[idx]
395
        logB = table["logB"][idx]
396
397
        # Compute properties
398
        pulsar = Pulsar(P0_birth, logB)
399
        snr = SNRTrueloveMcKee(e_sn=E_SN, n_ISM=n_ISM)
400
        pwn = PWN(pulsar, snr)
401
        r_out_pwn = pwn.radius(age).to_value("pc")
402
        L_PWN = pwn.luminosity_tev(age).to_value("erg")
403
        results.append(dict(r_out_pwn=r_out_pwn, L_PWN=L_PWN))
404
405
    # Add columns to table
406
    table["r_out_PWN"] = Column(
407
        [_["r_out_pwn"] for _ in results], unit="pc", description="PWN outer radius"
408
    )
409
    table["L_PWN"] = Column(
410
        [_["L_PWN"] for _ in results],
411
        unit="erg",
412
        description="PWN luminosity above 1 TeV",
413
    )
414
    return table
415
416
417
def add_observed_source_parameters(table):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (16/15).
Loading history...
418
    """Add observed source parameters to the table."""
419
    # Read relevant columns
420
    distance = table["distance"]
421
    r_in = table["r_in"]
422
    r_out = table["r_out"]
423
    r_out_PWN = table["r_out_PWN"]
424
    L_SNR = table["L_SNR"]
425
    L_PSR = table["L_PSR"]
426
    L_PWN = table["L_PWN"]
427
428
    # Compute properties
429
    ext_in_SNR = astrometry.radius_to_angle(r_in, distance)
430
    ext_out_SNR = astrometry.radius_to_angle(r_out, distance)
431
    ext_out_PWN = astrometry.radius_to_angle(r_out_PWN, distance)
432
433
    # Ellipse parameters not used for now
434
    theta = np.pi / 2 * np.ones(len(table))  # Position angle?
435
    epsilon = np.zeros(len(table))  # Ellipticity?
436
437
    S_SNR = astrometry.luminosity_to_flux(L_SNR, distance)
438
    # Ld2_PSR = astrometry.luminosity_to_flux(L_PSR, distance)
439
    Ld2_PSR = L_PSR / distance ** 2
440
    S_PWN = astrometry.luminosity_to_flux(L_PWN, distance)
441
442
    # Add columns
443
    table["ext_in_SNR"] = Column(ext_in_SNR, unit="deg")
444
    table["ext_out_SNR"] = Column(ext_out_SNR, unit="deg")
445
    table["ext_out_PWN"] = Column(ext_out_PWN, unit="deg")
446
    table["theta"] = Column(theta, unit="rad")
447
    table["epsilon"] = Column(epsilon, unit="")
448
    table["S_SNR"] = Column(S_SNR, unit="cm-2 s-1")
449
    table["Ld2_PSR"] = Column(Ld2_PSR, unit="erg s-1 kpc-2")
450
    table["S_PWN"] = Column(S_PWN, unit="cm-2 s-1")
451
    return table
452
453
454
def add_observed_parameters(table, obs_pos=None):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (20/15).
Loading history...
455
    """Add observable parameters (such as sky position or distance).
456
457
    Input table columns: x, y, z, extension, luminosity
458
459
    Output table columns: distance, glon, glat, flux, angular_extension
460
461
    Position of observer in cartesian coordinates.
462
    Center of galaxy as origin, x-axis goes trough sun.
463
464
    Parameters
465
    ----------
466
    table : `~astropy.table.Table`
467
        Input table
468
    obs_pos : tuple or None
469
        Observation position (X, Y, Z) in Galactocentric coordinates (default: Earth)
470
471
    Returns
472
    -------
473
    table : `~astropy.table.Table`
474
        Modified input table with columns added
475
    """
476
    obs_pos = obs_pos or [D_SUN_TO_GALACTIC_CENTER, 0, 0]
477
478
    # Get data
479
    x, y, z = table["x"].quantity, table["y"].quantity, table["z"].quantity
480
    vx, vy, vz = table["vx"].quantity, table["vy"].quantity, table["vz"].quantity
481
482
    distance, glon, glat = astrometry.galactic(x, y, z, obs_pos=obs_pos)
483
484
    # Compute projected velocity
485
    v_glon, v_glat = astrometry.velocity_glon_glat(x, y, z, vx, vy, vz)
486
487
    coordinate = SkyCoord(glon, glat, unit="deg", frame="galactic").transform_to("icrs")
488
    ra, dec = coordinate.ra.deg, coordinate.dec.deg
489
490
    # Add columns to table
491
    table["distance"] = Column(
492
        distance, unit="pc", description="Distance observer to source center"
493
    )
494
    table["GLON"] = Column(glon, unit="deg", description="Galactic longitude")
495
    table["GLAT"] = Column(glat, unit="deg", description="Galactic latitude")
496
    table["VGLON"] = Column(
497
        v_glon.to("deg/Myr"),
498
        unit="deg/Myr",
499
        description="Velocity in Galactic longitude",
500
    )
501
    table["VGLAT"] = Column(
502
        v_glat.to("deg/Myr"),
503
        unit="deg/Myr",
504
        description="Velocity in Galactic latitude",
505
    )
506
    table["RA"] = Column(ra, unit="deg", description="Right ascension")
507
    table["DEC"] = Column(dec, unit="deg", description="Declination")
508
509
    try:
510
        luminosity = table["luminosity"]
511
        flux = astrometry.luminosity_to_flux(luminosity, distance)
512
        table["flux"] = Column(flux.value, unit=flux.unit, description="Source flux")
513
    except KeyError:
514
        pass
515
516
    try:
517
        extension = table["extension"]
518
        angular_extension = np.degrees(np.arctan(extension / distance))
519
        table["angular_extension"] = Column(
520
            angular_extension,
521
            unit="deg",
522
            description="Source angular radius (i.e. half-diameter)",
523
        )
524
    except KeyError:
525
        pass
526
527
    return table
528