Passed
Pull Request — master (#37)
by
unknown
05:03
created

apexpy.apex.Apex.get_babs()   A

Complexity

Conditions 1

Size

Total Lines 23
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 23
rs 10
c 0
b 0
f 0
cc 1
nop 4
1
# -*- coding: utf-8 -*-
2
3
from __future__ import division, print_function, absolute_import
4
5
import os
6
import warnings
7
import datetime as dt
8
9
import numpy as np
10
11
from . import helpers
12
13
# below try..catch required for autodoc to work on readthedocs
14
try:
15
    from . import fortranapex as fa
16
except ImportError as err:
17
    print("ERROR: fortranapex module could not be imported, so apexpy probably"
18
          " won't work.  Make sure you have a gfortran compiler. Wheels "
19
          "installation assumes your compiler lives in /opt/local/bin")
20
    raise err
21
22
# make sure invalid warnings are always shown
23
warnings.filterwarnings('always', message='.*set to -9999 where*',
24
                        module='apexpy.apex')
25
26
27
class ApexHeightError(ValueError):
28
    pass
29
30
31
class Apex(object):
32
    """Performs coordinate conversions, field-line mapping and base vector
33
    calculations.
34
35
    Parameters
36
    ==========
37
    date : float, :class:`dt.date`, or :class:`dt.datetime`, optional
38
        Determines which IGRF coefficients are used in conversions. Uses
39
        current date as default.  If float, use decimal year.
40
    refh : float, optional
41
        Reference height in km for apex coordinates (the field lines are mapped
42
        to this height)
43
    datafile : str, optional
44
        Path to custom coefficient file
45
46
    Attributes
47
    ==========
48
    year : float
49
        Decimal year used for the IGRF model
50
    refh : float
51
        Reference height in km for apex coordinates
52
    datafile : str
53
        Path to coefficient file
54
55
    Notes
56
    =====
57
    The calculations use IGRF-12 with coefficients from 1900 to 2020 [1]_.
58
59
    References
60
    ==========
61
62
    .. [1] Thébault, E. et al. (2015), International Geomagnetic Reference
63
           Field: the 12th generation, Earth, Planets and Space, 67(1), 79,
64
           :doi:`10.1186/s40623-015-0228-9`.
65
66
    """
67
68
    def __init__(self, date=None, refh=0, datafile=None, fortranlib=None):
69
70
        if datafile is None:
71
            datafile = os.path.join(os.path.dirname(__file__), 'apexsh.dat')
72
73
        if fortranlib is None:
74
            fortranlib = fa.__file__
75
76
        self.RE = 6371.009  # mean Earth radius
77
        self.set_refh(refh)  # reference height
78
79
        if date is None:
80
            self.year = helpers.toYearFraction(dt.datetime.now())
81
        else:
82
            try:
83
                # convert date/datetime object to decimal year
84
                self.year = helpers.toYearFraction(date)
85
            except AttributeError:
86
                # Failed while finding datetime attribute, so
87
                # date is probably an int or float; use directly
88
                self.year = date
89
90
        if not os.path.isfile(datafile):
91
            raise IOError('Datafile does not exist: {}'.format(datafile))
92
93
        if not os.path.isfile(fortranlib):
94
            raise IOError('Datafile does not exist: {}'.format(fortranlib))
95
96
        self.datafile = datafile
97
        self.fortranlib = fortranlib
98
99
        self.set_epoch(self.year)
100
101
        # vectorize fortran functions
102
        self._geo2qd = np.frompyfunc(
103
            lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180,
104
                                                 height, 0)[:2], 3, 2)
105
        self._geo2apex = np.frompyfunc(
106
            lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360 -
107
                                                   180, height, self.refh,
108
                                                   0)[2:4], 3, 2)
109
        self._geo2apexall = np.frompyfunc(
110
            lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360 -
111
                                                   180, height, self.refh,
112
                                                   1), 3, 14)
113
        self._qd2geo = np.frompyfunc(
114
            lambda qlat, qlon, height, precision: fa.apxq2g(qlat, (qlon + 180)
115
                                                            % 360 - 180, height,
116
                                                            precision), 4, 3)
117
        self._basevec = np.frompyfunc(
118
            lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180,
119
                                                 height, 1)[2:4], 3, 2)
120
121
        # vectorize other nonvectorized functions
122
        self._apex2qd = np.frompyfunc(self._apex2qd_nonvectorized, 3, 2)
123
        self._qd2apex = np.frompyfunc(self._qd2apex_nonvectorized, 3, 2)
124
        self._get_babs = np.frompyfunc(self._get_babs_nonvectorized, 3, 1)
125
126
    def convert(self, lat, lon, source, dest, height=0, datetime=None,
127
                precision=1e-10, ssheight=50 * 6371):
128
        """Converts between geodetic, modified apex, quasi-dipole and MLT.
129
130
        Parameters
131
        ==========
132
        lat : array_like
133
            Latitude
134
        lon : array_like
135
            Longitude/MLT
136
        source : {'geo', 'apex', 'qd', 'mlt'}
137
            Input coordinate system
138
        dest : {'geo', 'apex', 'qd', 'mlt'}
139
            Output coordinate system
140
        height : array_like, optional
141
            Altitude in km
142
        datetime : :class:`datetime.datetime`
143
            Date and time for MLT conversions (required for MLT conversions)
144
        precision : float, optional
145
            Precision of output (degrees) when converting to geo. A negative
146
            value of this argument produces a low-precision calculation of
147
            geodetic lat/lon based only on their spherical harmonic
148
            representation.
149
            A positive value causes the underlying Fortran routine to iterate
150
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
151
            the input QD lat/lon to within the specified precision (all
152
            coordinates being converted to geo are converted to QD first and
153
            passed through APXG2Q).
154
        ssheight : float, optional
155
            Altitude in km to use for converting the subsolar point from
156
            geographic to magnetic coordinates. A high altitude is used
157
            to ensure the subsolar point is mapped to high latitudes, which
158
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
159
160
        Returns
161
        =======
162
        lat : ndarray or float
163
            Converted latitude (if converting to MLT, output latitude is apex)
164
        lat : ndarray or float
165
            Converted longitude/MLT
166
167
        """
168
169
        if datetime is None and ('mlt' in [source, dest]):
170
            raise ValueError('datetime must be given for MLT calculations')
171
172
        lat = helpers.checklat(lat)
173
174
        if source == dest:
175
            return lat, lon
176
        # from geo
177
        elif source == 'geo' and dest == 'apex':
178
            lat, lon = self.geo2apex(lat, lon, height)
179
        elif source == 'geo' and dest == 'qd':
180
            lat, lon = self.geo2qd(lat, lon, height)
181
        elif source == 'geo' and dest == 'mlt':
182
            lat, lon = self.geo2apex(lat, lon, height)
183
            lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
184
        # from apex
185
        elif source == 'apex' and dest == 'geo':
186
            lat, lon, _ = self.apex2geo(lat, lon, height, precision=precision)
187
        elif source == 'apex' and dest == 'qd':
188
            lat, lon = self.apex2qd(lat, lon, height=height)
189
        elif source == 'apex' and dest == 'mlt':
190
            lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
191
        # from qd
192
        elif source == 'qd' and dest == 'geo':
193
            lat, lon, _ = self.qd2geo(lat, lon, height, precision=precision)
194
        elif source == 'qd' and dest == 'apex':
195
            lat, lon = self.qd2apex(lat, lon, height=height)
196
        elif source == 'qd' and dest == 'mlt':
197
            lat, lon = self.qd2apex(lat, lon, height=height)
198
            lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
199
        # from mlt (input latitude assumed apex)
200
        elif source == 'mlt' and dest == 'geo':
201
            lon = self.mlt2mlon(lon, datetime, ssheight=ssheight)
202
            lat, lon, _ = self.apex2geo(lat, lon, height, precision=precision)
203
        elif source == 'mlt' and dest == 'apex':
204
            lon = self.mlt2mlon(lon, datetime, ssheight=ssheight)
205
        elif source == 'mlt' and dest == 'qd':
206
            lon = self.mlt2mlon(lon, datetime, ssheight=ssheight)
207
            lat, lon = self.apex2qd(lat, lon, height=height)
208
        # no other transformations are implemented
209
        else:
210
            estr = 'Unknown coordinate transformation: '
211
            estr += '{} -> {}'.format(source, dest)
212
            raise NotImplementedError(estr)
213
214
        return lat, lon
215
216
    def geo2apex(self, glat, glon, height):
217
        """Converts geodetic to modified apex coordinates.
218
219
        Parameters
220
        ==========
221
        glat : array_like
222
            Geodetic latitude
223
        glon : array_like
224
            Geodetic longitude
225
        height : array_like
226
            Altitude in km
227
228
        Returns
229
        =======
230
        alat : ndarray or float
231
            Modified apex latitude
232
        alon : ndarray or float
233
            Modified apex longitude
234
235
        """
236
237
        glat = helpers.checklat(glat, name='glat')
238
239
        alat, alon = self._geo2apex(glat, glon, height)
240
241
        if np.any(np.float64(alat) == -9999):
242
            warnings.warn('Apex latitude set to -9999 where undefined '
243
                          '(apex height may be < reference height)')
244
245
        # if array is returned, dtype is object, so convert to float
246
        return np.float64(alat), np.float64(alon)
247
248
    def apex2geo(self, alat, alon, height, precision=1e-10):
249
        """Converts modified apex to geodetic coordinates.
250
251
        Parameters
252
        ==========
253
        alat : array_like
254
            Modified apex latitude
255
        alon : array_like
256
            Modified apex longitude
257
        height : array_like
258
            Altitude in km
259
        precision : float, optional
260
            Precision of output (degrees). A negative value of this argument
261
            produces a low-precision calculation of geodetic lat/lon based only
262
            on their spherical harmonic representation. A positive value causes
263
            the underlying Fortran routine to iterate until feeding the output
264
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
265
            within the specified precision.
266
267
        Returns
268
        =======
269
        glat : ndarray or float
270
            Geodetic latitude
271
        glon : ndarray or float
272
            Geodetic longitude
273
        error : ndarray or float
274
            The angular difference (degrees) between the input QD coordinates
275
            and the qlat/qlon produced by feeding the output glat and glon
276
            into geo2qd (APXG2Q)
277
278
        """
279
280
        alat = helpers.checklat(alat, name='alat')
281
282
        qlat, qlon = self.apex2qd(alat, alon, height=height)
283
        glat, glon, error = self.qd2geo(qlat, qlon, height, precision=precision)
284
285
        return glat, glon, error
286
287
    def geo2qd(self, glat, glon, height):
288
        """Converts geodetic to quasi-dipole coordinates.
289
290
        Parameters
291
        ==========
292
        glat : array_like
293
            Geodetic latitude
294
        glon : array_like
295
            Geodetic longitude
296
        height : array_like
297
            Altitude in km
298
299
        Returns
300
        =======
301
        qlat : ndarray or float
302
            Quasi-dipole latitude
303
        qlon : ndarray or float
304
            Quasi-dipole longitude
305
306
        """
307
308
        glat = helpers.checklat(glat, name='glat')
309
310
        qlat, qlon = self._geo2qd(glat, glon, height)
311
312
        # if array is returned, dtype is object, so convert to float
313
        return np.float64(qlat), np.float64(qlon)
314
315
    def qd2geo(self, qlat, qlon, height, precision=1e-10):
316
        """Converts quasi-dipole to geodetic coordinates.
317
318
        Parameters
319
        ==========
320
        qlat : array_like
321
            Quasi-dipole latitude
322
        qlon : array_like
323
            Quasi-dipole longitude
324
        height : array_like
325
            Altitude in km
326
        precision : float, optional
327
            Precision of output (degrees). A negative value of this argument
328
            produces a low-precision calculation of geodetic lat/lon based only
329
            on their spherical harmonic representation. A positive value causes
330
            the underlying Fortran routine to iterate until feeding the output
331
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
332
            within the specified precision.
333
334
        Returns
335
        =======
336
        glat : ndarray or float
337
            Geodetic latitude
338
        glon : ndarray or float
339
            Geodetic longitude
340
        error : ndarray or float
341
            The angular difference (degrees) between the input QD coordinates
342
            and the qlat/qlon produced by feeding the output glat and glon
343
            into geo2qd (APXG2Q)
344
345
        """
346
347
        qlat = helpers.checklat(qlat, name='qlat')
348
349
        glat, glon, error = self._qd2geo(qlat, qlon, height, precision)
350
351
        # if array is returned, dtype is object, so convert to float
352
        return np.float64(glat), np.float64(glon), np.float64(error)
353
354 View Code Duplication
    def _apex2qd_nonvectorized(self, alat, alon, height):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
355
        """Convert from apex to quasi-dipole (not-vectorised)
356
357
        Parameters
358
        -----------
359
        alat : (float)
360
            Apex latitude in degrees
361
        alon : (float)
362
            Apex longitude in degrees
363
        height : (float)
364
            Height in km
365
366
        Returns
367
        ---------
368
        qlat : (float)
369
            Quasi-dipole latitude in degrees
370
        qlon : (float)
371
            Quasi-diplole longitude in degrees
372
        """
373
374
        alat = helpers.checklat(alat, name='alat')
375
376
        # convert modified apex to quasi-dipole:
377
        qlon = alon
378
379
        # apex height
380
        hA = self.get_apex(alat)
381
382
        if hA < height:
383
            if np.isclose(hA, height, rtol=0, atol=1e-5):
384
                # allow for values that are close
385
                hA = height
386
            else:
387
                estr = 'height {:.3g} is > apex height '.format(np.max(height))
388
                estr += '{:.3g} for alat {:.3g}'.format(hA, alat)
389
                raise ApexHeightError(estr)
390
391
        qlat = np.sign(alat) * np.degrees(np.arccos(np.sqrt((self.RE + height) /
392
                                                            (self.RE + hA))))
393
394
        return qlat, qlon
395
396
    def apex2qd(self, alat, alon, height):
397
        """Converts modified apex to quasi-dipole coordinates.
398
399
        Parameters
400
        ==========
401
        alat : array_like
402
            Modified apex latitude
403
        alon : array_like
404
            Modified apex longitude
405
        height : array_like
406
            Altitude in km
407
408
        Returns
409
        =======
410
        qlat : ndarray or float
411
            Quasi-dipole latitude
412
        qlon : ndarray or float
413
            Quasi-dipole longitude
414
415
        Raises
416
        ======
417
        ApexHeightError
418
            if `height` > apex height
419
420
        """
421
422
        qlat, qlon = self._apex2qd(alat, alon, height)
423
424
        # if array is returned, the dtype is object, so convert to float
425
        return np.float64(qlat), np.float64(qlon)
426
427 View Code Duplication
    def _qd2apex_nonvectorized(self, qlat, qlon, height):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
428
429
        qlat = helpers.checklat(qlat, name='qlat')
430
431
        alon = qlon
432
        hA = self.get_apex(qlat, height)  # apex height
433
434
        if hA < self.refh:
435
            if np.isclose(hA, self.refh, rtol=0, atol=1e-5):
436
                # allow for values that are close
437
                hA = self.refh
438
            else:
439
                estr = 'apex height ({:.3g}) is < reference height '.format(hA)
440
                estr += '({:.3g}) for qlat {:.3g}'.format(self.refh, qlat)
441
                raise ApexHeightError(estr)
442
443
        alat = np.sign(qlat) * np.degrees(np.arccos(np.sqrt((self.RE +
444
                                                             self.refh) /
445
                                                            (self.RE + hA))))
446
447
        return alat, alon
448
449
    def qd2apex(self, qlat, qlon, height):
450
        """Converts quasi-dipole to modified apex coordinates.
451
452
        Parameters
453
        ==========
454
        qlat : array_like
455
            Quasi-dipole latitude
456
        qlon : array_like
457
            Quasi-dipole longitude
458
        height : array_like
459
            Altitude in km
460
461
        Returns
462
        =======
463
        alat : ndarray or float
464
            Modified apex latitude
465
        alon : ndarray or float
466
            Modified apex longitude
467
468
        Raises
469
        ======
470
        ApexHeightError
471
            if apex height < reference height
472
473
        """
474
475
        alat, alon = self._qd2apex(qlat, qlon, height)
476
477
        # if array is returned, the dtype is object, so convert to float
478
        return np.float64(alat), np.float64(alon)
479
480
    def mlon2mlt(self, mlon, datetime, ssheight=50 * 6371):
481
        """Computes the magnetic local time at the specified magnetic longitude
482
        and UT.
483
484
        Parameters
485
        ==========
486
        mlon : array_like
487
            Magnetic longitude (apex and quasi-dipole longitude are always
488
            equal)
489
        datetime : :class:`datetime.datetime`
490
            Date and time
491
        ssheight : float, optional
492
            Altitude in km to use for converting the subsolar point from
493
            geographic to magnetic coordinates. A high altitude is used
494
            to ensure the subsolar point is mapped to high latitudes, which
495
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
496
497
        Returns
498
        =======
499
        mlt : ndarray or float
500
            Magnetic local time [0, 24)
501
502
        Notes
503
        =====
504
        To compute the MLT, we find the apex longitude of the subsolar point at
505
        the given time. Then the MLT of the given point will be computed from
506
        the separation in magnetic longitude from this point (1 hour = 15
507
        degrees).
508
509
        """
510
        ssglat, ssglon = helpers.subsol(datetime)
511
        ssalat, ssalon = self.geo2apex(ssglat, ssglon, ssheight)
512
513
        # np.float64 will ensure lists are converted to arrays
514
        return (180 + np.float64(mlon) - ssalon) / 15 % 24
515
516
    def mlt2mlon(self, mlt, datetime, ssheight=50 * 6371):
517
        """Computes the magnetic longitude at the specified magnetic local time
518
        and UT.
519
520
        Parameters
521
        ==========
522
        mlt : array_like
523
            Magnetic local time
524
        datetime : :class:`datetime.datetime`
525
            Date and time
526
        ssheight : float, optional
527
            Altitude in km to use for converting the subsolar point from
528
            geographic to magnetic coordinates. A high altitude is used
529
            to ensure the subsolar point is mapped to high latitudes, which
530
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
531
532
        Returns
533
        =======
534
        mlon : ndarray or float
535
            Magnetic longitude [0, 360) (apex and quasi-dipole longitude are
536
            always equal)
537
538
        Notes
539
        =====
540
        To compute the magnetic longitude, we find the apex longitude of the
541
        subsolar point at the given time. Then the magnetic longitude of the
542
        given point will be computed from the separation in magnetic local time
543
        from this point (1 hour = 15 degrees).
544
        """
545
546
        ssglat, ssglon = helpers.subsol(datetime)
547
        ssalat, ssalon = self.geo2apex(ssglat, ssglon, ssheight)
548
549
        # np.float64 will ensure lists are converted to arrays
550
        return (15 * np.float64(mlt) - 180 + ssalon + 360) % 360
551
552
    def map_to_height(self, glat, glon, height, newheight, conjugate=False,
553
                      precision=1e-10):
554
        """Performs mapping of points along the magnetic field to the closest
555
        or conjugate hemisphere.
556
557
        Parameters
558
        ==========
559
        glat : array_like
560
            Geodetic latitude
561
        glon : array_like
562
            Geodetic longitude
563
        height : array_like
564
            Source altitude in km
565
        newheight : array_like
566
            Destination altitude in km
567
        conjugate : bool, optional
568
            Map to `newheight` in the conjugate hemisphere instead of the
569
            closest hemisphere
570
        precision : float, optional
571
            Precision of output (degrees). A negative value of this argument
572
            produces a low-precision calculation of geodetic lat/lon based only
573
            on their spherical harmonic representation. A positive value causes
574
            the underlying Fortran routine to iterate until feeding the output
575
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
576
            within the specified precision.
577
578
        Returns
579
        =======
580
        newglat : ndarray or float
581
            Geodetic latitude of mapped point
582
        newglon : ndarray or float
583
            Geodetic longitude of mapped point
584
        error : ndarray or float
585
            The angular difference (degrees) between the input QD coordinates
586
            and the qlat/qlon produced by feeding the output glat and glon
587
            into geo2qd (APXG2Q)
588
589
        Notes
590
        =====
591
        The mapping is done by converting glat/glon/height to modified apex
592
        lat/lon, and converting back to geographic using newheight (if
593
        conjugate, use negative apex latitude when converting back)
594
595
        """
596
597
        alat, alon = self.geo2apex(glat, glon, height)
598
        if conjugate:
599
            alat = -alat
600
        try:
601
            newglat, newglon, error = self.apex2geo(alat, alon, newheight,
602
                                                    precision=precision)
603
        except ApexHeightError:
604
            raise ApexHeightError("newheight is > apex height")
605
606
        return newglat, newglon, error
607
608
    def _map_EV_to_height(self, alat, alon, height, newheight, X, EV):
609
610
        # make sure X is array of correct shape
611
        if (not (np.ndim(X) == 1 and np.size(X) == 3) and
612
                not (np.ndim(X) == 2 and np.shape(X)[0] == 3)):
613
            # raise ValueError because if passing e.g. a (6,) ndarray the
614
            # reshape below will work even though the input is invalid
615
            raise ValueError(EV + ' must be (3, N) or (3,) ndarray')
616
        X = np.reshape(X, (3, np.size(X) // 3))
617
618
        _, _, _, _, _, _, d1, d2, _, e1, e2, _ = self.basevectors_apex(alat, alon, height, coords='apex')
619
620
        if EV == 'E':
621
            v1 = e1
622
            v2 = e2
623
        else:
624
            v1 = d1
625
            v2 = d2
626
627
        # make sure v1 and v2 have shape (3, N)
628
        v1 = np.reshape(v1, (3, v1.size // 3))
629
        v2 = np.reshape(v2, (3, v2.size // 3))
630
631
        X1 = np.sum(X * v1, axis=0)  # E dot e1 or V dot d1
632
        X2 = np.sum(X * v2, axis=0)  # E dot e2 or V dot d2
633
634
        _, _, _, _, _, _, d1, d2, _, e1, e2, _ = self.basevectors_apex(alat, alon, newheight, coords='apex')
635
636
        if EV == 'E':
637
            v1 = d1
638
            v2 = d2
639
        else:
640
            v1 = e1
641
            v2 = e2
642
643
        # make sure v1 and v2 have shape (3, N)
644
        v1 = np.reshape(v1, (3, v1.size // 3))
645
        v2 = np.reshape(v2, (3, v2.size // 3))
646
647
        X_mapped = X1[np.newaxis, :] * v1 + X2[np.newaxis, :] * v2
648
649
        return np.squeeze(X_mapped)
650
651
    def map_E_to_height(self, alat, alon, height, newheight, E):
652
        """Performs mapping of electric field along the magnetic field.
653
654
        It is assumed that the electric field is perpendicular to B.
655
656
        Parameters
657
        ==========
658
        alat : (N,) array_like or float
659
            Modified apex latitude
660
        alon : (N,) array_like or float
661
            Modified apex longitude
662
        height : (N,) array_like or float
663
            Source altitude in km
664
        newheight : (N,) array_like or float
665
            Destination altitude in km
666
        E : (3,) or (3, N) array_like
667
            Electric field (at `alat`, `alon`, `height`) in geodetic east,
668
            north, and up components
669
670
        Returns
671
        =======
672
        E : (3, N) or (3,) ndarray
673
            The electric field at `newheight` (geodetic east, north, and up
674
            components)
675
676
        """
677
        return self._map_EV_to_height(alat, alon, height, newheight, E, 'E')
678
679
    def map_V_to_height(self, alat, alon, height, newheight, V):
680
        """Performs mapping of electric drift velocity along the magnetic field.
681
682
        It is assumed that the electric field is perpendicular to B.
683
684
        Parameters
685
        ==========
686
        alat : (N,) array_like or float
687
            Modified apex latitude
688
        alon : (N,) array_like or float
689
            Modified apex longitude
690
        height : (N,) array_like or float
691
            Source altitude in km
692
        newheight : (N,) array_like or float
693
            Destination altitude in km
694
        V : (3,) or (3, N) array_like
695
            Electric drift velocity (at `alat`, `alon`, `height`) in geodetic
696
            east, north, and up components
697
698
        Returns
699
        =======
700
        V : (3, N) or (3,) ndarray
701
            The electric drift velocity at `newheight` (geodetic east, north,
702
            and up components)
703
704
        """
705
706
        return self._map_EV_to_height(alat, alon, height, newheight, V, 'V')
707
708
    def basevectors_qd(self, lat, lon, height, coords='geo', precision=1e-10):
709
        """Returns quasi-dipole base vectors f1 and f2 at the specified
710
        coordinates.
711
712
        The vectors are described by Richmond [1995] [2]_ and
713
        Emmert et al. [2010] [3]_.  The vector components are geodetic east and
714
        north.
715
716
        Parameters
717
        ==========
718
        lat : (N,) array_like or float
719
            Latitude
720
        lon : (N,) array_like or float
721
            Longitude
722
        height : (N,) array_like or float
723
            Altitude in km
724
        coords : {'geo', 'apex', 'qd'}, optional
725
            Input coordinate system
726
        precision : float, optional
727
            Precision of output (degrees) when converting to geo. A negative
728
            value of this argument produces a low-precision calculation of
729
            geodetic lat/lon based only on their spherical harmonic
730
            representation.
731
            A positive value causes the underlying Fortran routine to iterate
732
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
733
            the input QD lat/lon to within the specified precision (all
734
            coordinates being converted to geo are converted to QD first and
735
            passed through APXG2Q).
736
737
        Returns
738
        =======
739
        f1 : (2, N) or (2,) ndarray
740
        f2 : (2, N) or (2,) ndarray
741
742
        References
743
        ==========
744
        .. [2] Richmond, A. D. (1995), Ionospheric Electrodynamics Using
745
               Magnetic Apex Coordinates, Journal of geomagnetism and
746
               geoelectricity, 47(2), 191–212, :doi:`10.5636/jgg.47.191`.
747
748
        .. [3] Emmert, J. T., A. D. Richmond, and D. P. Drob (2010),
749
               A computationally compact representation of Magnetic-Apex
750
               and Quasi-Dipole coordinates with smooth base vectors,
751
               J. Geophys. Res., 115(A8), A08322, :doi:`10.1029/2010JA015326`.
752
753
        """
754
755
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
756
                                  precision=precision)
757
758
        f1, f2 = self._basevec(glat, glon, height)
759
760
        # if inputs are not scalar, each vector is an array of arrays,
761
        # so reshape to a single array
762
        if f1.dtype == object:
763
            f1 = np.vstack(f1).T
764
            f2 = np.vstack(f2).T
765
766
        return f1, f2
767
768
    def basevectors_apex(self, lat, lon, height, coords='geo', precision=1e-10):
769
        """Returns base vectors in quasi-dipole and apex coordinates.
770
771
        The vectors are described by Richmond [1995] [4]_ and
772
        Emmert et al. [2010] [5]_.  The vector components are geodetic east,
773
        north, and up (only east and north for `f1` and `f2`).
774
775
        Parameters
776
        ==========
777
        lat, lon : (N,) array_like or float
778
            Latitude
779
        lat : (N,) array_like or float
780
            Longitude
781
        height : (N,) array_like or float
782
            Altitude in km
783
        coords : {'geo', 'apex', 'qd'}, optional
784
            Input coordinate system
785
        precision : float, optional
786
            Precision of output (degrees) when converting to geo. A negative
787
            value of this argument produces a low-precision calculation of
788
            geodetic lat/lon based only on their spherical harmonic
789
            representation.
790
            A positive value causes the underlying Fortran routine to iterate
791
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
792
            the input QD lat/lon to within the specified precision (all
793
            coordinates being converted to geo are converted to QD first and
794
            passed through APXG2Q).
795
796
        Returns
797
        =======
798
        f1, f2 : (2, N) or (2,) ndarray
799
        f3, g1, g2, g3, d1, d2, d3, e1, e2, e3 : (3, N) or (3,) ndarray
800
801
        Note
802
        ====
803
        `f3`, `g1`, `g2`, and `g3` are not part of the Fortran code
804
        by Emmert et al. [2010] [5]_. They are calculated by this
805
        Python library according to the following equations in
806
        Richmond [1995] [4]_:
807
808
        * `g1`: Eqn. 6.3
809
        * `g2`: Eqn. 6.4
810
        * `g3`: Eqn. 6.5
811
        * `f3`: Eqn. 6.8
812
813
        References
814
        ==========
815
816
        .. [4] Richmond, A. D. (1995), Ionospheric Electrodynamics Using
817
               Magnetic Apex Coordinates, Journal of geomagnetism and
818
               geoelectricity, 47(2), 191–212, :doi:`10.5636/jgg.47.191`.
819
820
        .. [5] Emmert, J. T., A. D. Richmond, and D. P. Drob (2010),
821
               A computationally compact representation of Magnetic-Apex
822
               and Quasi-Dipole coordinates with smooth base vectors,
823
               J. Geophys. Res., 115(A8), A08322, :doi:`10.1029/2010JA015326`.
824
825
        """
826
827
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
828
                                  precision=precision)
829
830
        returnvals = self._geo2apexall(glat, glon, height)
831
        qlat = np.float64(returnvals[0])
832
        alat = np.float64(returnvals[2])
833
        f1, f2 = returnvals[4:6]
834
        d1, d2, d3 = returnvals[7:10]
835
        e1, e2, e3 = returnvals[11:14]
836
837
        # if inputs are not scalar, each vector is an array of arrays,
838
        # so reshape to a single array
839
        if f1.dtype == object:
840
            f1 = np.vstack(f1).T
841
            f2 = np.vstack(f2).T
842
            d1 = np.vstack(d1).T
843
            d2 = np.vstack(d2).T
844
            d3 = np.vstack(d3).T
845
            e1 = np.vstack(e1).T
846
            e2 = np.vstack(e2).T
847
            e3 = np.vstack(e3).T
848
849
        # make sure arrays are 2D
850
        f1 = f1.reshape((2, f1.size // 2))
851
        f2 = f2.reshape((2, f2.size // 2))
852
        d1 = d1.reshape((3, d1.size // 3))
853
        d2 = d2.reshape((3, d2.size // 3))
854
        d3 = d3.reshape((3, d3.size // 3))
855
        e1 = e1.reshape((3, e1.size // 3))
856
        e2 = e2.reshape((3, e2.size // 3))
857
        e3 = e3.reshape((3, e3.size // 3))
858
859
        # compute f3, g1, g2, g3
860
        F1 = np.vstack((f1, np.zeros_like(f1[0])))
861
        F2 = np.vstack((f2, np.zeros_like(f2[0])))
862
        F = np.cross(F1.T, F2.T).T[-1]
863
        cosI = helpers.getcosIm(alat)
864
        k = np.array([0, 0, 1], dtype=np.float64).reshape((3, 1))
865
        g1 = ((self.RE + np.float64(height)) / (self.RE + self.refh)) ** (3 / 2) * d1 / F
866
        g2 = -1.0 / (2.0 * F * np.tan(np.radians(qlat))) * (k + ((self.RE + np.float64(height)) /
867
                                                                 (self.RE + self.refh)) * d2 / cosI)
868
        g3 = k * F
869
        f3 = np.cross(g1.T, g2.T).T
870
871
        if np.any(alat == -9999):
872
            warnings.warn(('Base vectors g, d, e, and f3 set to -9999 where '
873
                           'apex latitude is undefined (apex height may be < '
874
                           'reference height)'))
875
            f3 = np.where(alat == -9999, -9999, f3)
876
            g1 = np.where(alat == -9999, -9999, g1)
877
            g2 = np.where(alat == -9999, -9999, g2)
878
            g3 = np.where(alat == -9999, -9999, g3)
879
            d1 = np.where(alat == -9999, -9999, d1)
880
            d2 = np.where(alat == -9999, -9999, d2)
881
            d3 = np.where(alat == -9999, -9999, d3)
882
            e1 = np.where(alat == -9999, -9999, e1)
883
            e2 = np.where(alat == -9999, -9999, e2)
884
            e3 = np.where(alat == -9999, -9999, e3)
885
886
        return tuple(np.squeeze(x) for x in
887
                     [f1, f2, f3, g1, g2, g3, d1, d2, d3, e1, e2, e3])
888
889
    def get_apex(self, lat, height=None):
890
        """ Calculate apex height
891
892
        Parameters
893
        -----------
894
        lat : (float)
895
            Latitude in degrees
896
        height : (float or NoneType)
897
            Height above the surface of the earth in km or NoneType to use
898
            reference height (default=None)
899
900
        Returns
901
        ----------
902
        apex_height : (float)
903
            Height of the field line apex in km
904
        """
905
        lat = helpers.checklat(lat, name='alat')
906
        if height is None:
907
            height = self.refh
908
909
        cos_lat_squared = np.cos(np.radians(lat)) ** 2
910
        apex_height = (self.RE + height) / cos_lat_squared - self.RE
911
912
        return apex_height
913
914
    def set_epoch(self, year):
915
        """Updates the epoch for all subsequent conversions.
916
917
        Parameters
918
        ==========
919
        year : float
920
            Decimal year
921
922
        """
923
        # f2py
924
        self.year = np.float64(year)
925
        fa.loadapxsh(self.datafile, self.year)
926
        fa.cofrm(self.year)
927
928
    def set_refh(self, refh):
929
        """Updates the apex reference height for all subsequent conversions.
930
931
        Parameters
932
        ==========
933
        refh : float
934
            Apex reference height in km
935
936
        Notes
937
        =====
938
        The reference height is the height to which field lines will be mapped,
939
        and is only relevant for conversions involving apex (not quasi-dipole).
940
941
        """
942
        self.refh = refh
943
944
    def _get_babs_nonvectorized(self, glat, glon, height):
945
        bnorth, beast, bdown, babs = fa.feldg(1, glat, glon, height)
946
        # BABS is in guass, so convert to tesla
947
        return babs / 10000.0
948
949
    def get_babs(self, glat, glon, height):
950
        """Returns the magnitude of the IGRF magnetic field in tesla.
951
952
        Parameters
953
        ==========
954
        glat : array_like
955
            Geodetic latitude
956
        glon : array_like
957
            Geodetic longitude
958
        height : array_like
959
            Altitude in km
960
961
        Returns
962
        =======
963
        babs : ndarray or float
964
            Magnitude of the IGRF magnetic field
965
966
        """
967
968
        babs = self._get_babs(glat, glon, height)
969
970
        # if array is returned, the dtype is object, so convert to float
971
        return np.float64(babs)
972
973
    def bvectors_apex(self, lat, lon, height, coords='geo', precision=1e-10):
974
        """Returns the magnetic field vectors in apex coordinates.
975
976
        The apex magnetic field vectors described by Richmond [1995] [4]_ and
977
        Emmert et al. [2010] [5]_, specfically the Be3 and Bd3 components. The
978
        vector components are geodetic east, north, and up.
979
980
        Parameters
981
        ==========
982
        lat, lon : (N,) array_like or float
983
            Latitude
984
        lat : (N,) array_like or float
985
            Longitude
986
        height : (N,) array_like or float
987
            Altitude in km
988
        coords : {'geo', 'apex', 'qd'}, optional
989
            Input coordinate system
990
        precision : float, optional
991
            Precision of output (degrees) when converting to geo. A negative
992
            value of this argument produces a low-precision calculation of
993
            geodetic lat/lon based only on their spherical harmonic
994
            representation.
995
            A positive value causes the underlying Fortran routine to iterate
996
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
997
            the input QD lat/lon to within the specified precision (all
998
            coordinates being converted to geo are converted to QD first and
999
            passed through APXG2Q).
1000
1001
        Returns
1002
        =======
1003
        Be3: (1, N) or (1,) ndarray
1004
        e3 : (3, N) or (3,) ndarray
1005
        Bd3: (1, N) or (1,) ndarray
1006
        d3 : (3, N) or (3,) ndarray
1007
1008
        Note
1009
        ====
1010
        Be3 is not equivalent to the magnitude of the IGRF magnitude, but is
1011
        instead equal to the IGRF magnitude divided by a scaling factor, D.
1012
        Similarly, Bd3 is the IGRF magnitude multiplied by D.
1013
1014
        See Richmond, A. D. (1995) [4]_ equations 3.13 and 3.14
1015
1016
        References
1017
        ==========
1018
1019
        .. [4] Richmond, A. D. (1995), Ionospheric Electrodynamics Using
1020
               Magnetic Apex Coordinates, Journal of geomagnetism and
1021
               geoelectricity, 47(2), 191–212, :doi:`10.5636/jgg.47.191`.
1022
1023
        .. [5] Emmert, J. T., A. D. Richmond, and D. P. Drob (2010),
1024
               A computationally compact representation of Magnetic-Apex
1025
               and Quasi-Dipole coordinates with smooth base vectors,
1026
               J. Geophys. Res., 115(A8), A08322, :doi:`10.1029/2010JA015326`.
1027
1028
        """
1029
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
1030
                                  precision=precision)
1031
1032
        babs = self.get_babs(glat, glon, height)
1033
1034
        _, _, _, _, _, _, d1, d2, d3, _, _, e3 = self.basevectors_apex(glat, glon, height, coords='geo')
1035
        d1_cross_d2 = np.cross(d1.T, d2.T).T
1036
        D = np.sqrt(np.sum(d1_cross_d2 ** 2, axis=0))
1037
1038
        Be3 = babs / D
1039
        Bd3 = babs * D
1040
1041
        return Be3, e3, Bd3, d3
1042