Passed
Pull Request — master (#8)
by Angeline
01:22
created

apexpy.apex.Apex._apex2qd_nonvectorized()   B

Complexity

Conditions 3

Size

Total Lines 41
Code Lines 13

Duplication

Lines 41
Ratio 100 %

Importance

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