Completed
Push — master ( 04a3aa...668558 )
by Ben
56s
created

Ellipsoid   A

Complexity

Total Complexity 4

Size/Duplication

Total Lines 29
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 10
c 0
b 0
f 0
wmc 4

3 Methods

Rating   Name   Duplication   Size   Complexity  
A in_ellipsoid() 0 13 2
A transform() 0 4 1
A __init__() 0 6 1
1
"Free electron density model"
2
import os
3
from functools import partial
4
5
import numpy as np
6
from astropy.table import Table
7
from numpy import cos
8
from numpy import cosh
9
from numpy import exp
10
from numpy import pi
11
from numpy import sqrt
12
from numpy import tan
13
from scipy.integrate import quad as integrate
14
15
from .utils import ClassOperation
16
from .utils import galactic_to_galactocentric
17
from .utils import lzproperty
18
from .utils import rotation
19
20
# import astropy.units as us
21
# from astropy.coordinates import SkyCoord
22
23
# Configuration
24
# TODO: use to config file
25
# input parameters for large-scale components of NE2001 30 June '02
26
# flags = {'wg1': 1,
27
#          'wg2': 1,
28
#          'wga': 1,
29
#          'wggc': 1,
30
#          'wglism': 1,
31
#          'wgcN': 1,
32
#          'wgvN': 1}
33
34
# solar_params = {'Rsun': 8.3}
35
36
# spiral_arms_params = {'na': 0.028,
37
#                       'ha': 0.23,
38
#                       'wa': 0.65,
39
#                       'Aa': 10.5,
40
#                       'Fa': 5,
41
#                       'narm1': 0.5,
42
#                       'narm2': 1.2,
43
#                       'narm3': 1.3,
44
#                       'narm4': 1.0,
45
#                       'narm5': 0.25,
46
#                       'warm1': 1.0,
47
#                       'warm2': 1.5,
48
#                       'warm3': 1.0,
49
#                       'warm4': 0.8,
50
#                       'warm5': 1.0,
51
#                       'harm1': 1.0,
52
#                       'harm2': 0.8,
53
#                       'harm3': 1.3,
54
#                       'harm4': 1.5,
55
#                       'harm5': 1.0,
56
#                       'farm1': 1.1,
57
#                       'farm2': 0.3,
58
#                       'farm3': 0.4,
59
#                       'farm4': 1.5,
60
#                       'farm5': 0.3}
61
62
PARAMS = {
63
    'thick_disk': {'e_density': 0.033/0.97,
64
                   'height': 0.97,
65
                   'radius': 17.5,
66
                   'F': 0.18},
67
68
    'thin_disk': {'e_density': 0.08,
69
                  'height': 0.15,
70
                  'radius': 3.8,
71
                  'F': 120},
72
73
    'galactic_center': {'e_density': 10.0,
74
                        'center': np.array([-0.01, 0.0, -0.020]),
75
                        'radius': 0.145,
76
                        'height': 0.026,
77
                        'F': 0.6e5},
78
79
    'ldr': {'ellipsoid': np.array([1.50, .750, .50]),
80
            'center': np.array([1.36, 8.06, 0.0]),
81
            'theta': -24.2*pi/180,
82
            'e_density': 0.012,
83
            'F': 0.1},
84
85
    'lsb': {'ellipsoid': np.array([1.050, .4250, .3250]),
86
            'center': np.array([-0.75, 9.0, -0.05]),
87
            'theta': 139.*pi/180,
88
            'e_density': 0.016,
89
            'F': 0.01},
90
91
    'lhb': {'cylinder': np.array([.0850, .1000, .330]),
92
            'center': np.array([0.01, 8.45, 0.17]),
93
            'theta': 15*pi/180,
94
            'e_density': 0.005,
95
            'F': 0.01},
96
97
    'loop_in': {'center': np.array([-0.045, 8.40, 0.07]),
98
                'radius': 0.120,
99
                'e_density': 0.0125,
100
                'F': 0.2},
101
102
    'loop_out': {'center': np.array([-0.045, 8.40, 0.07]),
103
                 'radius': 0.120 + 0.060,
104
                 'e_density': 0.0125,
105
                 'F': 0.01}}
106
107
108
def thick_disk(xyz, r_sun, radius, height):
109
    """
110
    Calculate the contribution of the thick disk to the free electron density
111
     at x, y, z = `xyz`
112
    """
113
    r_ratio = sqrt(xyz[0]**2 + xyz[1]**2)/radius
114
    return (cos(r_ratio*pi/2)/cos(r_sun*pi/2/radius) /
115
            cosh(xyz[-1]/height)**2 *
116
            (r_ratio < 1))
117
118
119
def thin_disk(xyz, radius, height):
120
    """
121
    Calculate the contribution of the thin disk to the free electron density
122
     at x, y, z = `xyz`
123
    """
124
    r_ratio = sqrt(xyz[0]**2 + xyz[1]**2)/radius
125
    return (exp(-(1 - r_ratio)**2*radius**2/1.8**2) /
126
            cosh(xyz[-1]/height)**2)  # Why 1.8?
127
128
129
def gc(xyz, center, radius, height):
130
    """
131
    Calculate the contribution of the Galactic center to the free
132
    electron density at x, y, z = `xyz`
133
    """
134
    # Here I'm using the expression in the NE2001 code which is inconsistent
135
    # with Cordes and Lazio 2011 (0207156v3) (See Table 2)
136
    try:
137
        xyz = xyz - center
138
    except ValueError:
139
        xyz = xyz - center[:, None]
140
141
    r_ratio = sqrt(xyz[0]**2 + xyz[1]**2)/radius
142
143
    # ????
144
    # Cordes and Lazio 2011 (0207156v3) (Table 2)
145
    # return ne_gc0*exp(-(r2d/rgc)**2 - (xyz[-1]/hgc)**2)
146
    # ????
147
148
    # Constant ne (form NE2001 code)
149
    return (r_ratio**2 + (xyz[-1]/height)**2 < 1)*(r_ratio <= 1)
150
151
152
class NEobject(ClassOperation):
153
    """
154
    A general electron density object
155
    """
156
157
    def __init__(self, func, **params):
158
        """
159
160
        Arguments:
161
        - `xyz`: Location where the electron density is calculated
162
        - `func`: Electron density function
163
        - `**params`: Model parameter
164
        """
165
        self._fparam = params.pop('F')
166
        self._ne0 = params.pop('e_density')
167
        try:
168
            self._func = func(**params)
169
        except TypeError:
170
            self._func = partial(func, **params)
171
        self._params = params
172
173
    def DM(self, xyz, xyz_sun=np.array([0, 8.5, 0]),
174
           epsrel=1e-4, epsabs=1e-6,
175
           *arg, **kwargs):
176
        """
177
        Calculate the dispersion measure at location `xyz`
178
        """
179
        xyz = xyz - xyz_sun
180
181
        dfinal = sqrt(np.sum(xyz**2, axis=0))
182
        return integrate(lambda x: self.ne(xyz_sun + x*xyz),
183
                         0, 1, *arg, epsrel=epsrel, epsabs=epsabs,
184
                         **kwargs)[0]*dfinal*1000
185
186
    def ne(self, *args):
187
        return self.electron_density(*args)
188
189
    def electron_density(self, xyz):
190
        "Electron density at the location `xyz`"
191
        return self._ne0*self._func(xyz)
192
193
    @property
194
    def F(self, xyz):
195
        "Fluctuation parameter"
196
        return (self.ne(xyz) > 0)*self._fparam
197
198
199
class NEcombine(NEobject):
200
    """
201
    """
202
203
    def __init__(self, object1, object2):
204
        """
205
        """
206
        self._object1 = object1
207
        self._object2 = object2
208
209
    def electron_density(self, *args):
210
        ne1 = self._object1.ne(*args)
211
        ne2 = self._object2.ne(*args)
212
        return ne1 + ne2*(ne1 <= 0)
213
214
215
class LocalISM(NEobject):
216
    """
217
    Calculate the contribution of the local ISM
218
    to the free electron density at x, y, z = `xyz`
219
    """
220
221
    def __init__(self, **params):
222
        """
223
        """
224
        self.ldr = NEobject(in_ellipsoid, **params['ldr'])
225
        self.lsb = NEobject(in_ellipsoid, **params['lsb'])
226
        self.lhb = NEobject(in_cylinder, **params['lhb'])
227
        self.loop_in = NEobject(in_half_sphere, **params['loop_in'])
228
        self.loop_out = NEobject(in_half_sphere, **params['loop_out'])
229
230
        self.loop = NEcombine(self.loop_in, self.loop_out)
231
        self._lism = NEcombine(self.lhb,
232
                               NEcombine(self.loop,
233
                                         NEcombine(self.lsb, self.ldr)))
234
235
    def electron_density(self, xyz):
236
        """
237
        Calculate the contribution of the local ISM to the free
238
        electron density at x, y, z = `xyz`
239
        """
240
        return self._lism.ne(xyz)
241
242
243
class Clumps(NEobject):
244
    """
245
    """
246
247
    def __init__(self, clumps_file=None):
248
        """
249
        """
250
        if not clumps_file:
251
            this_dir, _ = os.path.split(__file__)
252
            clumps_file = os.path.join(this_dir, "data", "neclumpN.NE2001.dat")
253
        self._data = Table.read(clumps_file, format='ascii')
254
255
    @lzproperty
256
    def use_clump(self):
257
        """
258
        """
259
        return np.array(self._data['flag']) == 0
260
261
    @lzproperty
262
    def xyz(self):
263
        """
264
        """
265
        return self.get_xyz()
266
267
    @lzproperty
268
    def gl(self):
269
        """
270
        Galactic longitude (deg)
271
        """
272
        return np.array(self._data['l'])
273
274
    @lzproperty
275
    def gb(self):
276
        """
277
        Galactic latitude (deg)
278
        """
279
        return np.array(self._data['b'])
280
281
    @lzproperty
282
    def distance(self):
283
        """
284
        Distance from the sun (kpc)
285
        """
286
        return np.array(self._data['dc'])
287
288
    @lzproperty
289
    def radius2(self):
290
        """
291
        Radius of the clump (kpc)
292
        """
293
        return np.array(self._data['rc']**2)
294
295
    @lzproperty
296
    def ne0(self):
297
        """
298
        Electron density of each clump (cm^{-3})
299
        """
300
        return np.array(self._data['nec'])
301
302
    @lzproperty
303
    def ne0_use(self):
304
        """
305
        Electron density of each clump (cm^{-3})
306
        """
307
        return self.ne0*self.use_clump
308
309
    @lzproperty
310
    def edge(self):
311
        """
312
        Clump edge
313
        0 => use exponential rolloff out to 5 clump radii
314
        1 => uniform and truncated at 1/e clump radius
315
        """
316
        return np.array(self._data['edge'])
317
318
    def get_xyz(self, rsun=8.5):
319
        """
320
        """
321
        # xyz = SkyCoord(frame="galactic", l=self.gl, b=self.gb,
322
        #                distance=self.distance,
323
        #                z_sun = z_sun*us.kpc,
324
        #                unit="deg, deg, kpc").galactocentric.
325
        #                                      cartesian.xyz.value
326
        # return xyz
327
        return galactic_to_galactocentric(l=self.gl, b=self.gb,
328
                                          distance=self.distance, rsun=rsun)
329
330
    def clump_factor(self, xyz):
331
        """
332
        Clump edge
333
        0 => use exponential rolloff out to 5 clump radii
334
        1 => uniform and truncated at 1/e clump radius
335
        """
336
        if xyz.ndim == 1:
337
            return clump_factor(xyz, self.xyz, self.radius2, self.edge)
338
        else:
339
            xyz = xyz[:, :, None] - self.xyz[:, None, :]
340
341
        q2 = (np.sum(xyz**2, axis=0) /
342
              self.radius2)
343
        # NOTE: In the original NE2001 code q2 <= 5 is used instead of q <= 5.
344
        # TODO: check this
345
        q5 = (q2 <= 5)*(self.edge == 0)
346
        res = np.zeros_like(q2)
347
        res[(q2 <= 1)*(self.edge == 1)] = 1
348
        res[q5] = exp(-q2[q5])
349
        return res
350
351
    def electron_density(self, xyz):
352
        """
353
        The contribution of the clumps to the free
354
        electron density at x, y, z = `xyz`
355
        """
356
        return np.sum(self.clump_factor(xyz)*self.ne0_use, axis=-1)
357
358
359
class Voids(NEobject):
360
    """
361
    """
362
363
    def __init__(self, voids_file=None):
364
        """
365
        """
366
        if not voids_file:
367
            this_dir, _ = os.path.split(__file__)
368
            voids_file = os.path.join(this_dir, "data", "nevoidN.NE2001.dat")
369
        self._data = Table.read(voids_file, format='ascii')
370
371
    @lzproperty
372
    def use_void(self):
373
        """
374
        """
375
        return np.array(self._data['flag'] == 0)
376
377
    @lzproperty
378
    def xyz(self):
379
        """
380
        """
381
        return self.get_xyz()
382
383
    @lzproperty
384
    def xyz_rot(self):
385
        """
386
        """
387
        return np.array([R.dot(xyzi) for R,
388
                         xyzi in zip(self.rotation, self.xyz.T)]).T
389
390
    @lzproperty
391
    def gl(self):
392
        """
393
        Galactic longitude (deg)
394
        """
395
        return np.array(self._data['l'])
396
397
    @lzproperty
398
    def gb(self):
399
        """
400
        Galactic latitude (deg)
401
        """
402
        return np.array(self._data['b'])
403
404
    @lzproperty
405
    def distance(self):
406
        """
407
        Distance from the sun (kpc)
408
        """
409
        return np.array(self._data['dv'])
410
411
    @lzproperty
412
    def ellipsoid_abc(self):
413
        """
414
        Void axis
415
        """
416
        return np.array([self._data['aav'],
417
                         self._data['bbv'],
418
                         self._data['ccv']])
419
420
    @lzproperty
421
    def rotation(self):
422
        """
423
        Rotation and rescaling matrix
424
        """
425
        return np.array([
426
            (rotation(thetaz*pi/180, -1).dot(
427
                rotation(thetay*pi/180, 1)).T/abc).T
428
            for thetaz, thetay, abc
429
            in zip(self._data['thvz'], self._data['thvy'],
430
                   self.ellipsoid_abc.T)
431
        ])
432
433
    @lzproperty
434
    def ne0(self):
435
        """
436
        Electron density of each void (cm^{-3})
437
        """
438
        return np.array(self._data['nev'])
439
440
    @lzproperty
441
    def ne0_use(self):
442
        """
443
        Electron density of each void (cm^{-3})
444
        """
445
        return self.ne0*self.use_void
446
447
    @lzproperty
448
    def edge(self):
449
        """
450
        Void edge
451
        0 => use exponential rolloff out to 5 clump radii
452
        1 => uniform and truncated at 1/e clump radius
453
        """
454
        return np.array(self._data['edge'])
455
456
    def get_xyz(self, rsun=8.5):
457
        """
458
        """
459
        # xyz = SkyCoord(frame="galactic", l=self.gl, b=self.gb,
460
        #                distance=self.distance,
461
        #                z_sun = z_sun*us.kpc,
462
        #                unit="deg, deg, kpc").galactocentric.
463
        #                cartesian.xyz.value
464
        # return xyz
465
        return galactic_to_galactocentric(l=self.gl, b=self.gb,
466
                                          distance=self.distance, rsun=rsun)
467
468
    def void_factor(self, xyz):
469
        """
470
        Clump edge
471
        0 => use exponential rolloff out to 5 clump radii
472
        1 => uniform and truncated at 1/e clump radius
473
        """
474
        xyz = (self.rotation.dot(xyz).T - self.xyz_rot).T
475
476
        q2 = np.sum(xyz**2, axis=1).T
477
        # NOTE: In the original NE2001 code q2 <= 5 is used instead of q <= 5.
478
        # TODO: check this
479
        return (q2 <= 1)*self.edge + (q2 <= 5)*(1-self.edge)*exp(-q2)
480
481
    def electron_density(self, xyz):
482
        """
483
        The contribution of the clumps to the free
484
        electron density at x, y, z = `xyz`
485
        """
486
        return np.sum(self.void_factor(xyz)*self.ne0_use, axis=-1)
487
488
489
class ElectronDensity(NEobject):
490
    """
491
    A class holding all the elements which contribute to free electron density
492
    """
493
494
    def __init__(self, r_sun=8.5, clumps_file=None, voids_file=None,
495
                 **params):
496
        """
497
        """
498
        self._params = params
499
        self._thick_disk = NEobject(thick_disk, r_sun=r_sun,
500
                                    **params['thick_disk'])
501
        self._thin_disk = NEobject(thin_disk, **params['thin_disk'])
502
        self._galactic_center = NEobject(gc, **params['galactic_center'])
503
        self._lism = LocalISM(**params)
504
        self._clumps = Clumps(clumps_file=clumps_file)
505
        self._voids = Voids(voids_file=voids_file)
506
        self._combined = (NEcombine(self._voids,
507
                                    NEcombine(self._lism,
508
                                              self._thick_disk +
509
                                              self._thin_disk +
510
                                              self._galactic_center)) +
511
                          self._clumps)
512
513
    def electron_density(self, xyz):
514
        return self._combined.ne(xyz)
515
516
517
class Ellipsoid(object):
518
    """
519
    """
520
521
    def __init__(self, center, ellipsoid, theta):
522
        """
523
        """
524
        self.center = center
525
        self.ellipsoid = ellipsoid
526
        self.theta = theta
527
528
    @lzproperty
529
    def transform(self):
530
        "Rotation and rescaling matrix"
531
        return (rotation(self.theta, -1).T/self.ellipsoid).T
532
533
    def in_ellipsoid(self, xyz):
534
        """
535
        Test if xyz in the ellipsoid
536
        Theta in radians
537
        """
538
        try:
539
            xyz = xyz - self.center
540
        except ValueError:
541
            xyz = xyz - self.center[:, None]
542
543
        xyz = self.transform.dot(xyz)
544
545
        return np.sum(xyz**2, axis=0) <= 1
546
547
548
def in_ellipsoid(center, ellipsoid, theta):
549
    return Ellipsoid(center, ellipsoid, theta).in_ellipsoid
550
551
552
def in_cylinder(xyz, center, cylinder, theta):
553
    """
554
    Test if xyz in the cylinder
555
    Theta in radians
556
    """
557
    xyz0 = xyz.copy()
558
    try:
559
        xyz = xyz - center
560
    except ValueError:
561
        xyz = xyz - center[:, None]
562
        cylinder = np.vstack([cylinder]*xyz.shape[-1]).T
563
    xyz[1] -= tan(theta)*xyz0[-1]
564
565
    cylinder_p = cylinder.copy()
566
    z_c = (center[-1] - cylinder[-1])
567
    izz = (xyz0[-1] <= 0)*(xyz0[-1] >= z_c)
568
    cylinder_p[0] = (0.001 +
569
                     (cylinder[0] - 0.001) *
570
                     (1 - xyz0[-1]/z_c))*izz + cylinder[0]*(~izz)
571
    xyz_p = xyz/cylinder_p
572
573
    return (xyz_p[0]**2 + xyz_p[1]**2 <= 1) * (xyz_p[-1]**2 <= 1)
574
575
576
def in_half_sphere(xyz, center, radius):
577
    "Test if `xyz` in the sphere with radius r_sphere  centerd at `xyz_center`"
578
    xyz0 = xyz.copy()
579
    try:
580
        xyz = xyz - center
581
    except ValueError:
582
        xyz = xyz - center[:, None]
583
    distance = sqrt(np.sum(xyz**2, axis=0))
584
    return (distance <= radius)*(xyz0[-1] >= 0)
585
586
587
def clump_factor(xyz, xyz0, r2, edge):
588
    """
589
    Clump edge
590
    0 => use exponential rolloff out to 5 clump radii
591
    1 => uniform and truncated at 1/e clump radius
592
    """
593
    xyz = (xyz - xyz0.T).T
594
595
    q2 = (xyz[0]**2 + xyz[1]**2 + xyz[2]**2) / r2
596
    # NOTE: In the original NE2001 code q2 <= 5 is used instead of q <= 5.
597
    # TODO: check this
598
    q5 = (q2 <= 5)*(edge == 0)
599
    res = np.zeros_like(q2)
600
    res[(q2 <= 1)*(edge == 1)] = 1
601
    res[q5] = exp(-q2[q5])
602
    return res
603