apexpy.apex.Apex.map_E_to_height()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 30
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 30
rs 10
c 0
b 0
f 0
cc 1
nop 6
1
# -*- coding: utf-8 -*-
2
"""Classes that make up the core of apexpy."""
3
4
import datetime as dt
5
import numpy as np
6
import os
7
import warnings
8
from importlib import resources
9
10
from apexpy import helpers
11
12
# Below try..catch required for autodoc to work on readthedocs
13
try:
14
    from apexpy import fortranapex as fa
15
except ImportError as ierr:
16
    warnings.warn("".join(["fortranapex module could not be imported, so ",
17
                           "apexpy probably won't work.  Make sure you have ",
18
                           "a gfortran compiler. Failed with error: ",
19
                           str(ierr)]))
20
21
# Make sure invalid warnings are always shown
22
warnings.filterwarnings('always', message='.*set to NaN where*',
23
                        module='apexpy.apex')
24
25
26
class ApexHeightError(ValueError):
27
    """Specialized ValueError definition, to be used when apex height is wrong.
28
    """
29
    pass
30
31
32
class Apex(object):
33
    """Calculates coordinate conversions, field-line mapping, and base vectors.
34
35
    Parameters
36
    ----------
37
    date : NoneType, 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.  If None, uses
40
        current UTC. (default=None)
41
    refh : float, optional
42
        Reference height in km for apex coordinates, the field lines are mapped
43
        to this height. (default=0)
44
    datafile : str or NoneType, optional
45
        Path to custom coefficient file, if None uses `apexsh.dat` file
46
        (default=None)
47
    fortranlib : str or NoneType, optional
48
        Path to Fortran Apex CPython library, if None uses linked library file
49
        (default=None)
50
51
    Attributes
52
    ----------
53
    year : float
54
        Decimal year used for the IGRF model
55
    RE : float
56
        Earth radius in km, defaults to mean Earth radius
57
    refh : float
58
        Reference height in km for apex coordinates
59
    datafile : str
60
        Path to coefficient file
61
    fortranlib : str
62
        Path to Fortran Apex CPython library
63
    igrf_fn : str
64
        IGRF coefficient filename
65
66
    Notes
67
    -----
68
    The calculations use IGRF-14 with coefficients from 1900 to 2030 [1]_.
69
70
    The geodetic reference ellipsoid is WGS84.
71
72
    References
73
    ----------
74
75
    .. [1] Thébault, E. et al. (2015), International Geomagnetic Reference
76
           Field: the 12th generation, Earth, Planets and Space, 67(1), 79,
77
           :doi:`10.1186/s40623-015-0228-9`.
78
79
    """
80
81
    # ------------------------
82
    # Define the magic methods
83
84
    def __init__(self, date=None, refh=0, datafile=None, fortranlib=None):
85
86
        self.RE = 6371.009  # Mean Earth radius in km
87
        self.set_refh(refh)  # Reference height in km
88
89
        if date is None:
90
            self.year = helpers.toYearFraction(dt.datetime.now(
91
                tz=dt.timezone.utc))
92
        else:
93
            try:
94
                # Convert date/datetime object to decimal year
95
                self.year = helpers.toYearFraction(date)
96
            except AttributeError:
97
                # Failed while finding datetime attribute, so
98
                # date is probably an int or float; use directly
99
                self.year = date
100
101
        # If datafile is not specified, use the package default, otherwise
102
        # check that the provided file exists
103
        if datafile is None:
104
            datafile = os.path.join(resources.files(__package__), 'apexsh.dat')
105
        else:
106
            if not os.path.isfile(datafile):
107
                raise IOError('Data file does not exist: {}'.format(datafile))
108
109
        # If fortranlib is not specified, use the package default, otherwise
110
        # check that the provided file exists
111
        if fortranlib is None:
112
            fortranlib = fa.__file__
113
        else:
114
            if not os.path.isfile(fortranlib):
115
                raise IOError('Fortran library does not exist: {}'.format(
116
                    fortranlib))
117
118
        self.datafile = datafile
119
        self.fortranlib = fortranlib
120
121
        # Set the IGRF coefficient text file name
122
        self.igrf_fn = os.path.join(resources.files(__package__),
123
                                    'igrf14coeffs.txt')
124
125
        # Update the Fortran epoch using the year defined above
126
        self.set_epoch(self.year)
127
128
        # Vectorize the fortran functions
129
        self._geo2qd = np.frompyfunc(
130
            lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180,
131
                                                 height, 0)[:2], 3, 2)
132
        self._geo2apex = np.frompyfunc(
133
            lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360
134
                                                   - 180, height, self.refh,
135
                                                   0)[2:4], 3, 2)
136
        self._geo2apexall = np.frompyfunc(
137
            lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360
138
                                                   - 180, height, self.refh,
139
                                                   1), 3, 14)
140
        self._qd2geo = np.frompyfunc(
141
            lambda qlat, qlon, height, precision: fa.apxq2g(qlat, (qlon + 180)
142
                                                            % 360 - 180, height,
143
                                                            precision), 4, 3)
144
        self._basevec = np.frompyfunc(
145
            lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180,
146
                                                 height, 1)[2:4], 3, 2)
147
148
        # Vectorize other nonvectorized functions
149
        self._apex2qd = np.frompyfunc(self._apex2qd_nonvectorized, 3, 2)
150
        self._qd2apex = np.frompyfunc(self._qd2apex_nonvectorized, 3, 2)
151
        self._get_babs = np.frompyfunc(self._get_babs_nonvectorized, 3, 1)
152
153
        return
154
155
    def __repr__(self):
156
        """Produce an evaluatable representation of the Apex class."""
157
        rstr = "".join(["apexpy.Apex(date=", self.year.__repr__(), ", refh=",
158
                        self.refh.__repr__(), ", datafile=",
159
                        self.datafile.__repr__(), ", fortranlib=",
160
                        self.fortranlib.__repr__(), ")"])
161
162
        return rstr
163
164
    def __str__(self):
165
        """Produce a user-friendly representation of the Apex class."""
166
167
        out_str = '\n'.join(["Apex class conversions performed with",
168
                             "-------------------------------------",
169
                             "Decimal year: {:.8f}".format(self.year),
170
                             "Reference height: {:.3f} km".format(self.refh),
171
                             "Earth radius: {:.3f} km".format(self.RE), '',
172
                             "Coefficient file: '{:s}'".format(self.datafile),
173
                             "Cython Fortran library: '{:s}'".format(
174
                                 self.fortranlib)])
175
        return out_str
176
177
    def __eq__(self, comp_obj):
178
        """Performs equivalency evaluation.
179
180
        Parameters
181
        ----------
182
        comp_obj
183
            Object of any time to be compared to the class object
184
185
        Returns
186
        -------
187
        bool or NotImplemented
188
            True if self and comp_obj are identical, False if they are not,
189
            and NotImplemented if the classes are not the same
190
191
        """
192
193
        if isinstance(comp_obj, self.__class__):
194
            # Objects are the same if all the attributes are the same
195
            for apex_attr in ['year', 'refh', 'RE', 'datafile', 'fortranlib',
196
                              'igrf_fn']:
197
                bad_attr = False
198
                if hasattr(self, apex_attr):
199
                    aval = getattr(self, apex_attr)
200
201
                    if hasattr(comp_obj, apex_attr):
202
                        cval = getattr(comp_obj, apex_attr)
203
204
                        if cval != aval:
205
                            # Not equal, as the attribute values differ
206
                            bad_attr = True
207
                    else:
208
                        # The comparison object is missing an attribute
209
                        bad_attr = True
210
                else:
211
                    if hasattr(comp_obj, apex_attr):
212
                        # The current object is missing an attribute
213
                        bad_attr = True
214
215
                if bad_attr:
216
                    return False
217
218
            # If we arrive here, all expected attributes exist in both classes
219
            # and have the same values
220
            return True
221
222
        return NotImplemented
223
224
    # -------------------------
225
    # Define the hidden methods
226
227 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...
228
        """Convert from apex to quasi-dipole (not-vectorised)
229
230
        Parameters
231
        -----------
232
        alat : (float)
233
            Apex latitude in degrees
234
        alon : (float)
235
            Apex longitude in degrees
236
        height : (float)
237
            Height in km
238
239
        Returns
240
        ---------
241
        qlat : (float)
242
            Quasi-dipole latitude in degrees
243
        qlon : (float)
244
            Quasi-diplole longitude in degrees
245
246
        """
247
        # Evaluate the latitude
248
        alat = helpers.checklat(alat, name='alat')
249
250
        # Convert modified apex to quasi-dipole, longitude is the same
251
        qlon = alon
252
253
        # Get the apex height
254
        h_apex = self.get_apex(alat)
255
256
        if h_apex < height:
257
            if np.isclose(h_apex, height, rtol=0, atol=1e-5):
258
                # Allow for values that are close
259
                h_apex = height
260
            else:
261
                estr = ''.join(['height {:.3g} is > '.format(np.max(height)),
262
                                'apex height {:.3g} for alat {:.3g}'.format(
263
                                    h_apex, alat)])
264
                raise ApexHeightError(estr)
265
266
        # Convert the latitude
267
        salat = np.sign(alat) if alat != 0 else 1
268
        qlat = salat * np.degrees(np.arccos(np.sqrt((self.RE + height)
269
                                                    / (self.RE + h_apex))))
270
271
        return qlat, qlon
272
273 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...
274
        """Converts quasi-dipole to modified apex coordinates.
275
276
        Parameters
277
        ----------
278
        qlat : float
279
            Quasi-dipole latitude
280
        qlon : float
281
            Quasi-dipole longitude
282
        height : float
283
            Altitude in km
284
285
        Returns
286
        -------
287
        alat : float
288
            Modified apex latitude
289
        alon : float
290
            Modified apex longitude
291
292
        Raises
293
        ------
294
        ApexHeightError
295
            if apex height < reference height
296
297
        """
298
        # Evaluate the latitude
299
        qlat = helpers.checklat(qlat, name='qlat')
300
301
        # Get the longitude and apex height
302
        alon = qlon
303
        h_apex = self.get_apex(qlat, height)
304
305
        if h_apex < self.refh:
306
            if np.isclose(h_apex, self.refh, rtol=0, atol=1e-5):
307
                # Allow for values that are close
308
                h_apex = self.refh
309
            else:
310
                estr = ''.join(['apex height ({:.3g}) is < '.format(h_apex),
311
                                'reference height ({:.3g})'.format(self.refh),
312
                                ' for qlat {:.3g}'.format(qlat)])
313
                raise ApexHeightError(estr)
314
315
        # Convert the latitude
316
        sqlat = np.sign(qlat) if qlat != 0 else 1
317
        alat = sqlat * np.degrees(np.arccos(np.sqrt((self.RE + self.refh)
318
                                                    / (self.RE + h_apex))))
319
320
        return alat, alon
321
322
    def _map_EV_to_height(self, alat, alon, height, newheight, data, ev_flag):
323
        """Maps electric field related values to a desired height
324
325
        Parameters
326
        ----------
327
        alat : array-like
328
            Apex latitude in degrees.
329
        alon : array-like
330
            Apex longitude in degrees.
331
        height : array-like
332
            Current altitude in km.
333
        new_height : array-like
334
            Desired altitude to which EV values will be mapped in km.
335
        data : array-like
336
            3D value(s) for the electric field or electric drift
337
        ev_flag : str
338
           Specify if value is an electric field ('E') or electric drift ('V')
339
340
        Returns
341
        -------
342
        data_mapped : array-like
343
            Data mapped along the magnetic field from the old height to the
344
            new height.
345
346
        Raises
347
        ------
348
        ValueError
349
            If an unknown `ev_flag` or badly shaped data input is supplied.
350
351
        """
352
        # Ensure the E-V flag is correct
353
        ev_flag = ev_flag.upper()
354
        if ev_flag not in ['E', 'V']:
355
            raise ValueError('unknown electric field/drift flag')
356
357
        # Make sure data is array of correct shape
358
        if not (np.ndim(data) == 1
359
                and np.size(data) == 3) and not (np.ndim(data) == 2
360
                                                 and np.shape(data)[0] == 3):
361
            # Raise ValueError because if passing e.g. a (6,) ndarray the
362
            # reshape below will work even though the input is invalid
363
            raise ValueError('{:} must be (3, N) or (3,) ndarray'.format(
364
                ev_flag))
365
366
        data = np.reshape(data, (3, np.size(data) // 3))
367
368
        # Get the necessary base vectors at the current and new height
369
        v1 = list()
370
        v2 = list()
371
        for i, alt in enumerate([height, newheight]):
372
            _, _, _, _, _, _, d1, d2, _, e1, e2, _ = self.basevectors_apex(
373
                alat, alon, alt, coords='apex')
374
375
            if ev_flag == 'E' and i == 0 or ev_flag == 'V' and i == 1:
376
                v1.append(e1)
377
                v2.append(e2)
378
            else:
379
                v1.append(d1)
380
                v2.append(d2)
381
382
            # Make sure v1 and v2 have shape (3, N)
383
            v1[-1] = np.reshape(v1[-1], (3, v1[-1].size // 3))
384
            v2[-1] = np.reshape(v2[-1], (3, v2[-1].size // 3))
385
386
        # Take the dot product between the data value and each base vector
387
        # at the current height
388
        data1 = np.sum(data * v1[0], axis=0)
389
        data2 = np.sum(data * v2[0], axis=0)
390
391
        # Map the data to the new height, removing any axes of length 1
392
        # after the calculation
393
        data_mapped = np.squeeze(data1[np.newaxis, :] * v1[1]
394
                                 + data2[np.newaxis, :] * v2[1])
395
396
        return data_mapped
397
398
    def _get_babs_nonvectorized(self, glat, glon, height):
399
        """Get the absolute value of the B-field in Tesla
400
401
        Parameters
402
        ----------
403
        glat : float
404
            Geodetic latitude in degrees
405
        glon : float
406
            Geodetic longitude in degrees
407
        height : float
408
            Altitude in km
409
410
        Returns
411
        -------
412
        babs : float
413
            Absolute value of the magnetic field in Tesla
414
415
        """
416
        # Evaluate the latitude
417
        glat = helpers.checklat(glat, name='qlat')
418
419
        # Get the magnetic field output: North, East, Down, Absolute value
420
        bout = fa.feldg(1, glat, glon, height)
421
422
        # Convert the absolute value from Gauss to Tesla
423
        babs = bout[-1] / 10000.0
424
425
        return babs
426
427
    # -----------------------
428
    # Define the user methods
429
430
    def convert(self, lat, lon, source, dest, height=0, datetime=None,
431
                precision=1e-10, ssheight=50 * 6371):
432
        """Converts between geodetic, modified apex, quasi-dipole and MLT.
433
434
        Parameters
435
        ----------
436
        lat : array_like
437
            Latitude in degrees
438
        lon : array_like
439
            Longitude in degrees or MLT in hours
440
        source : str
441
            Input coordinate system, accepts 'geo', 'apex', 'qd', or 'mlt'
442
        dest : str
443
            Output coordinate system, accepts 'geo', 'apex', 'qd', or 'mlt'
444
        height : array_like, optional
445
            Altitude in km
446
        datetime : :class:`datetime.datetime`
447
            Date and time for MLT conversions (required for MLT conversions)
448
        precision : float, optional
449
            Precision of output (degrees) when converting to geo. A negative
450
            value of this argument produces a low-precision calculation of
451
            geodetic lat/lon based only on their spherical harmonic
452
            representation.
453
            A positive value causes the underlying Fortran routine to iterate
454
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
455
            the input QD lat/lon to within the specified precision (all
456
            coordinates being converted to geo are converted to QD first and
457
            passed through APXG2Q).
458
        ssheight : float, optional
459
            Altitude in km to use for converting the subsolar point from
460
            geographic to magnetic coordinates. A high altitude is used
461
            to ensure the subsolar point is mapped to high latitudes, which
462
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
463
464
        Returns
465
        -------
466
        lat : ndarray or float
467
            Converted latitude (if converting to MLT, output latitude is apex)
468
        lon : ndarray or float
469
            Converted longitude or MLT
470
471
        Raises
472
        ------
473
        ValueError
474
            For unknown source or destination coordinate system or a missing
475
            or badly formed latitude or datetime input
476
477
        """
478
        # Test the input values
479
        source = source.lower()
480
        dest = dest.lower()
481
482
        if source not in ['geo', 'apex', 'qd', 'mlt'] \
483
           or dest not in ['geo', 'apex', 'qd', 'mlt']:
484
            estr = 'Unknown coordinate transformation: {} -> {}'.format(source,
485
                                                                        dest)
486
            raise ValueError(estr)
487
488
        if datetime is None and ('mlt' in [source, dest]):
489
            raise ValueError('datetime must be given for MLT calculations')
490
491
        lat = helpers.checklat(lat)
492
493
        if source == dest:
494
            # The source and destination are the same, no conversion necessary
495
            return lat, lon
496
        else:
497
            # Select the correct conversion method
498
            if source == 'geo':
499
                if dest == 'qd':
500
                    lat, lon = self.geo2qd(lat, lon, height)
501
                else:
502
                    lat, lon = self.geo2apex(lat, lon, height)
503
                    if dest == 'mlt':
504
                        lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
505
            elif source == 'apex':
506
                if dest == 'geo':
507
                    lat, lon, _ = self.apex2geo(lat, lon, height,
508
                                                precision=precision)
509
                elif dest == 'qd':
510
                    lat, lon = self.apex2qd(lat, lon, height=height)
511
                elif dest == 'mlt':
512
                    lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
513
            elif source == 'qd':
514
                if dest == 'geo':
515
                    lat, lon, _ = self.qd2geo(lat, lon, height,
516
                                              precision=precision)
517
                else:
518
                    lat, lon = self.qd2apex(lat, lon, height=height)
519
                    if dest == 'mlt':
520
                        lon = self.mlon2mlt(lon, datetime, ssheight=ssheight)
521
            elif source == 'mlt':
522
                # From MLT means that the input latitude is assumed to be apex,
523
                # so we don't need to update latitude for apex conversions.
524
                lon = self.mlt2mlon(lon, datetime, ssheight=ssheight)
525
                if dest == 'geo':
526
                    lat, lon, _ = self.apex2geo(lat, lon, height,
527
                                                precision=precision)
528
                elif dest == 'qd':
529
                    lat, lon = self.apex2qd(lat, lon, height=height)
530
531
        return lat, lon
532
533
    def geo2apex(self, glat, glon, height):
534
        """Converts geodetic to modified apex coordinates.
535
536
        Parameters
537
        ----------
538
        glat : array_like
539
            Geodetic latitude
540
        glon : array_like
541
            Geodetic longitude
542
        height : array_like
543
            Altitude in km
544
545
        Returns
546
        -------
547
        alat : ndarray or float
548
            Modified apex latitude
549
        alon : ndarray or float
550
            Modified apex longitude
551
552
        """
553
554
        glat = helpers.checklat(glat, name='glat')
555
556
        alat, alon = self._geo2apex(glat, glon, height)
557
558
        if np.any(alat == -9999):
559
            warnings.warn(''.join(['Apex latitude set to NaN where undefined ',
560
                                   '(apex height may be < reference height)']))
561
            if np.isscalar(alat):
562
                alat = np.nan
563
            else:
564
                alat[alat == -9999] = np.nan
565
566
        # If array is returned, dtype is object, so convert to float
567
        alat = helpers.set_array_float(alat)
568
        alon = helpers.set_array_float(alon)
569
570
        return alat, alon
571
572
    def apex2geo(self, alat, alon, height, precision=1e-10):
573
        """Converts modified apex to geodetic coordinates.
574
575
        Parameters
576
        ----------
577
        alat : array_like
578
            Modified apex latitude
579
        alon : array_like
580
            Modified apex longitude
581
        height : array_like
582
            Altitude in km
583
        precision : float, optional
584
            Precision of output (degrees). A negative value of this argument
585
            produces a low-precision calculation of geodetic lat/lon based only
586
            on their spherical harmonic representation. A positive value causes
587
            the underlying Fortran routine to iterate until feeding the output
588
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
589
            within the specified precision.
590
591
        Returns
592
        -------
593
        glat : ndarray or float
594
            Geodetic latitude
595
        glon : ndarray or float
596
            Geodetic longitude
597
        error : ndarray or float
598
            The angular difference (degrees) between the input QD coordinates
599
            and the qlat/qlon produced by feeding the output glat and glon
600
            into geo2qd (APXG2Q)
601
602
        """
603
        # Evaluate the latitude
604
        alat = helpers.checklat(alat, name='alat')
605
606
        # Perform the two-step convertion to geodetic coordinates
607
        qlat, qlon = self.apex2qd(alat, alon, height=height)
608
        glat, glon, error = self.qd2geo(qlat, qlon, height, precision=precision)
609
610
        return glat, glon, error
611
612
    def geo2qd(self, glat, glon, height):
613
        """Converts geodetic to quasi-dipole coordinates.
614
615
        Parameters
616
        ----------
617
        glat : array_like
618
            Geodetic latitude
619
        glon : array_like
620
            Geodetic longitude
621
        height : array_like
622
            Altitude in km
623
624
        Returns
625
        -------
626
        qlat : ndarray or float
627
            Quasi-dipole latitude
628
        qlon : ndarray or float
629
            Quasi-dipole longitude
630
631
        """
632
        # Evaluate the latitude
633
        glat = helpers.checklat(glat, name='glat')
634
635
        # Convert to quasi-dipole coordinates
636
        qlat, qlon = self._geo2qd(glat, glon, height)
637
638
        # If array is returned, dtype is object, so convert to float
639
        qlat = helpers.set_array_float(qlat)
640
        qlon = helpers.set_array_float(qlon)
641
642
        return qlat, qlon
643
644
    def qd2geo(self, qlat, qlon, height, precision=1e-10):
645
        """Converts quasi-dipole to geodetic coordinates.
646
647
        Parameters
648
        ----------
649
        qlat : array_like
650
            Quasi-dipole latitude
651
        qlon : array_like
652
            Quasi-dipole longitude
653
        height : array_like
654
            Altitude in km
655
        precision : float, optional
656
            Precision of output (degrees). A negative value of this argument
657
            produces a low-precision calculation of geodetic lat/lon based only
658
            on their spherical harmonic representation. A positive value causes
659
            the underlying Fortran routine to iterate until feeding the output
660
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
661
            within the specified precision.
662
663
        Returns
664
        -------
665
        glat : ndarray or float
666
            Geodetic latitude
667
        glon : ndarray or float
668
            Geodetic longitude
669
        error : ndarray or float
670
            The angular difference (degrees) between the input QD coordinates
671
            and the qlat/qlon produced by feeding the output glat and glon
672
            into geo2qd (APXG2Q)
673
674
        """
675
        # Evaluate the latitude
676
        qlat = helpers.checklat(qlat, name='qlat')
677
678
        # Convert to geodetic coordinates
679
        glat, glon, error = self._qd2geo(qlat, qlon, height, precision)
680
681
        # If array is returned, dtype is object, so convert to float
682
        glat = helpers.set_array_float(glat)
683
        glon = helpers.set_array_float(glon)
684
        error = helpers.set_array_float(error)
685
686
        return glat, glon, error
687
688
    def apex2qd(self, alat, alon, height):
689
        """Converts modified apex to quasi-dipole coordinates.
690
691
        Parameters
692
        ----------
693
        alat : array_like
694
            Modified apex latitude
695
        alon : array_like
696
            Modified apex longitude
697
        height : array_like
698
            Altitude in km
699
700
        Returns
701
        -------
702
        qlat : ndarray or float
703
            Quasi-dipole latitude
704
        qlon : ndarray or float
705
            Quasi-dipole longitude
706
707
        Raises
708
        ------
709
        ApexHeightError
710
            if `height` > apex height
711
712
        """
713
        # Convert the latitude to apex, latitude check performed in the
714
        # hidden method
715
        qlat, qlon = self._apex2qd(alat, alon, height)
716
717
        # If array is returned, the dtype is object, so convert to float
718
        qlat = helpers.set_array_float(qlat)
719
        qlon = helpers.set_array_float(qlon)
720
721
        return qlat, qlon
722
723
    def qd2apex(self, qlat, qlon, height):
724
        """Converts quasi-dipole to modified apex coordinates.
725
726
        Parameters
727
        ----------
728
        qlat : array_like
729
            Quasi-dipole latitude
730
        qlon : array_like
731
            Quasi-dipole longitude
732
        height : array_like
733
            Altitude in km
734
735
        Returns
736
        -------
737
        alat : ndarray or float
738
            Modified apex latitude
739
        alon : ndarray or float
740
            Modified apex longitude
741
742
        Raises
743
        ------
744
        ApexHeightError
745
            if apex height < reference height
746
747
        """
748
        # Perform the conversion from quasi-dipole to apex coordinates
749
        alat, alon = self._qd2apex(qlat, qlon, height)
750
751
        # If array is returned, the dtype is object, so convert to float
752
        alat = helpers.set_array_float(alat)
753
        alon = helpers.set_array_float(alon)
754
755
        return alat, alon
756
757 View Code Duplication
    def mlon2mlt(self, mlon, dtime, ssheight=318550):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
758
        """Computes the magnetic local time at the specified magnetic longitude
759
        and UT.
760
761
        Parameters
762
        ----------
763
        mlon : array_like
764
            Magnetic longitude (apex and quasi-dipole longitude are always
765
            equal)
766
        dtime : :class:`datetime.datetime`
767
            Date and time
768
        ssheight : float, optional
769
            Altitude in km to use for converting the subsolar point from
770
            geographic to magnetic coordinates. A high altitude is used
771
            to ensure the subsolar point is mapped to high latitudes, which
772
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
773
            The current default is  50 * 6371, roughly 50 RE. (default=318550)
774
775
        Returns
776
        -------
777
        mlt : ndarray or float
778
            Magnetic local time in hours [0, 24)
779
780
        Notes
781
        -----
782
        To compute the MLT, we find the apex longitude of the subsolar point at
783
        the given time. Then the MLT of the given point will be computed from
784
        the separation in magnetic longitude from this point (1 hour = 15
785
        degrees).
786
787
        """
788
        # Get the subsolar location
789
        ssglat, ssglon = helpers.subsol(dtime)
790
791
        # Convert the subsolar location to apex coordinates
792
        _, ssalon = self.geo2apex(ssglat, ssglon, ssheight)
793
794
        # Calculate the magnetic local time (0-24 h range) from apex longitude.
795
        # Ensure lists are converted to arrays
796
        mlt = (180 + np.asarray(mlon) - ssalon) / 15 % 24
797
798
        if mlt.shape == ():
799
            mlt = np.float64(mlt)
800
801
        return mlt
802
803 View Code Duplication
    def mlt2mlon(self, mlt, dtime, ssheight=318550):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
804
        """Computes the magnetic longitude at the specified MLT and UT.
805
806
        Parameters
807
        ----------
808
        mlt : array_like
809
            Magnetic local time
810
        dtime : :class:`datetime.datetime`
811
            Date and time
812
        ssheight : float, optional
813
            Altitude in km to use for converting the subsolar point from
814
            geographic to magnetic coordinates. A high altitude is used
815
            to ensure the subsolar point is mapped to high latitudes, which
816
            prevents the South-Atlantic Anomaly (SAA) from influencing the MLT.
817
            The current default is  50 * 6371, roughly 50 RE. (default=318550)
818
819
        Returns
820
        -------
821
        mlon : ndarray or float
822
            Magnetic longitude [0, 360) (apex and quasi-dipole longitude are
823
            always equal)
824
825
        Notes
826
        -----
827
        To compute the magnetic longitude, we find the apex longitude of the
828
        subsolar point at the given time. Then the magnetic longitude of the
829
        given point will be computed from the separation in magnetic local time
830
        from this point (1 hour = 15 degrees).
831
        """
832
        # Get the location of the subsolar point at this time
833
        ssglat, ssglon = helpers.subsol(dtime)
834
835
        # Convert the location of the subsolar point to apex coordinates
836
        _, ssalon = self.geo2apex(ssglat, ssglon, ssheight)
837
838
        # Calculate the magnetic longitude (0-360 h range) from MLT.
839
        # Ensure lists are converted to arrays
840
        mlon = (15 * np.asarray(mlt) - 180 + ssalon + 360) % 360
841
842
        if mlon.shape == ():
843
            mlon = np.float64(mlon)
844
845
        return mlon
846
847
    def map_to_height(self, glat, glon, height, newheight, conjugate=False,
848
                      precision=1e-10):
849
        """Performs mapping of points along the magnetic field to the closest
850
        or conjugate hemisphere.
851
852
        Parameters
853
        ----------
854
        glat : array_like
855
            Geodetic latitude
856
        glon : array_like
857
            Geodetic longitude
858
        height : array_like
859
            Source altitude in km
860
        newheight : array_like
861
            Destination altitude in km
862
        conjugate : bool, optional
863
            Map to `newheight` in the conjugate hemisphere instead of the
864
            closest hemisphere
865
        precision : float, optional
866
            Precision of output (degrees). A negative value of this argument
867
            produces a low-precision calculation of geodetic lat/lon based only
868
            on their spherical harmonic representation. A positive value causes
869
            the underlying Fortran routine to iterate until feeding the output
870
            geo lat/lon into geo2qd (APXG2Q) reproduces the input QD lat/lon to
871
            within the specified precision.
872
873
        Returns
874
        -------
875
        newglat : ndarray or float
876
            Geodetic latitude of mapped point
877
        newglon : ndarray or float
878
            Geodetic longitude of mapped point
879
        error : ndarray or float
880
            The angular difference (degrees) between the input QD coordinates
881
            and the qlat/qlon produced by feeding the output glat and glon
882
            into geo2qd (APXG2Q)
883
884
        Notes
885
        -----
886
        The mapping is done by converting glat/glon/height to modified apex
887
        lat/lon, and converting back to geographic using newheight (if
888
        conjugate, use negative apex latitude when converting back)
889
890
        """
891
892
        alat, alon = self.geo2apex(glat, glon, height)
893
        if conjugate:
894
            alat = -alat
895
        try:
896
            newglat, newglon, error = self.apex2geo(alat, alon, newheight,
897
                                                    precision=precision)
898
        except ApexHeightError:
899
            raise ApexHeightError("input height is > apex height")
900
901
        return newglat, newglon, error
902
903
    def map_E_to_height(self, alat, alon, height, newheight, edata):
904
        """Performs mapping of electric field along the magnetic field.
905
906
        It is assumed that the electric field is perpendicular to B.
907
908
        Parameters
909
        ----------
910
        alat : (N,) array_like or float
911
            Modified apex latitude
912
        alon : (N,) array_like or float
913
            Modified apex longitude
914
        height : (N,) array_like or float
915
            Source altitude in km
916
        newheight : (N,) array_like or float
917
            Destination altitude in km
918
        edata : (3,) or (3, N) array_like
919
            Electric field (at `alat`, `alon`, `height`) in geodetic east,
920
            north, and up components
921
922
        Returns
923
        -------
924
        out : (3, N) or (3,) ndarray
925
            The electric field at `newheight` (geodetic east, north, and up
926
            components)
927
928
        """
929
        # Call hidden mapping method with flag for the electric field
930
        out = self._map_EV_to_height(alat, alon, height, newheight, edata, 'E')
931
932
        return out
933
934
    def map_V_to_height(self, alat, alon, height, newheight, vdata):
935
        """Performs mapping of electric drift velocity along the magnetic field.
936
937
        It is assumed that the electric field is perpendicular to B.
938
939
        Parameters
940
        ----------
941
        alat : (N,) array_like or float
942
            Modified apex latitude
943
        alon : (N,) array_like or float
944
            Modified apex longitude
945
        height : (N,) array_like or float
946
            Source altitude in km
947
        newheight : (N,) array_like or float
948
            Destination altitude in km
949
        vdata : (3,) or (3, N) array_like
950
            Electric drift velocity (at `alat`, `alon`, `height`) in geodetic
951
            east, north, and up components
952
953
        Returns
954
        -------
955
        out : (3, N) or (3,) ndarray
956
            The electric drift velocity at `newheight` (geodetic east, north,
957
            and up components)
958
959
        """
960
        # Call hidden mapping method with flag for the electric drift velocities
961
        out = self._map_EV_to_height(alat, alon, height, newheight, vdata, 'V')
962
963
        return out
964
965
    def basevectors_qd(self, lat, lon, height, coords='geo', precision=1e-10):
966
        """Get quasi-dipole base vectors f1 and f2 at the specified coordinates.
967
968
        Parameters
969
        ----------
970
        lat : (N,) array_like or float
971
            Latitude
972
        lon : (N,) array_like or float
973
            Longitude
974
        height : (N,) array_like or float
975
            Altitude in km
976
        coords : {'geo', 'apex', 'qd'}, optional
977
            Input coordinate system
978
        precision : float, optional
979
            Precision of output (degrees) when converting to geo. A negative
980
            value of this argument produces a low-precision calculation of
981
            geodetic lat/lon based only on their spherical harmonic
982
            representation.
983
            A positive value causes the underlying Fortran routine to iterate
984
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
985
            the input QD lat/lon to within the specified precision (all
986
            coordinates being converted to geo are converted to QD first and
987
            passed through APXG2Q).
988
989
        Returns
990
        -------
991
        f1 : (2, N) or (2,) ndarray
992
        f2 : (2, N) or (2,) ndarray
993
994
        Notes
995
        -----
996
        The vectors are described by Richmond [1995] [2]_ and
997
        Emmert et al. [2010] [3]_.  The vector components are geodetic east and
998
        north.
999
1000
        References
1001
        ----------
1002
        .. [2] Richmond, A. D. (1995), Ionospheric Electrodynamics Using
1003
               Magnetic Apex Coordinates, Journal of geomagnetism and
1004
               geoelectricity, 47(2), 191–212, :doi:`10.5636/jgg.47.191`.
1005
1006
        .. [3] Emmert, J. T., A. D. Richmond, and D. P. Drob (2010),
1007
               A computationally compact representation of Magnetic-Apex
1008
               and Quasi-Dipole coordinates with smooth base vectors,
1009
               J. Geophys. Res., 115(A8), A08322, doi:10.1029/2010JA015326.
1010
1011
        """
1012
        # Convert from current coordinates to geodetic coordinates
1013
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
1014
                                  precision=precision)
1015
1016
        # Get the geodetic base vectors
1017
        f1, f2 = self._basevec(glat, glon, height)
1018
1019
        # If inputs are not scalar, each vector is an array of arrays,
1020
        # so reshape to a single array
1021
        if f1.dtype == object:
1022
            f1 = np.vstack(f1).T
1023
            f2 = np.vstack(f2).T
1024
1025
        return f1, f2
1026
1027
    def basevectors_apex(self, lat, lon, height, coords='geo', precision=1e-10):
1028
        """Returns base vectors in quasi-dipole and apex coordinates.
1029
1030
        Parameters
1031
        ----------
1032
        lat : array_like or float
1033
            Latitude in degrees; input must be broadcastable with `lon` and
1034
            `height`.
1035
        lon : array_like or float
1036
            Longitude in degrees; input must be broadcastable with `lat` and
1037
            `height`.
1038
        height : array_like or float
1039
            Altitude in km; input must be broadcastable with `lon` and `lat`.
1040
        coords : str, optional
1041
            Input coordinate system, expects one of 'geo', 'apex', or 'qd'
1042
            (default='geo')
1043
        precision : float, optional
1044
            Precision of output (degrees) when converting to geo. A negative
1045
            value of this argument produces a low-precision calculation of
1046
            geodetic lat/lon based only on their spherical harmonic
1047
            representation.
1048
            A positive value causes the underlying Fortran routine to iterate
1049
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
1050
            the input QD lat/lon to within the specified precision (all
1051
            coordinates being converted to geo are converted to QD first and
1052
            passed through APXG2Q).
1053
1054
        Returns
1055
        -------
1056
        f1 : (3, N) or (3,) ndarray
1057
            Quasi-dipole base vector equivalent to e1, tangent to contours of
1058
            constant lambdaA
1059
        f2 : (3, N) or (3,) ndarray
1060
            Quasi-dipole base vector equivalent to e2
1061
        f3 : (3, N) or (3,) ndarray
1062
            Quasi-dipole base vector equivalent to e3, tangent to contours of
1063
            PhiA
1064
        g1 : (3, N) or (3,) ndarray
1065
            Quasi-dipole base vector equivalent to d1
1066
        g2 : (3, N) or (3,) ndarray
1067
            Quasi-dipole base vector equivalent to d2
1068
        g3 : (3, N) or (3,) ndarray
1069
            Quasi-dipole base vector equivalent to d3
1070
        d1 : (3, N) or (3,) ndarray
1071
            Apex base vector normal to contours of constant PhiA
1072
        d2 : (3, N) or (3,) ndarray
1073
            Apex base vector that completes the right-handed system
1074
        d3 : (3, N) or (3,) ndarray
1075
            Apex base vector normal to contours of constant lambdaA
1076
        e1 : (3, N) or (3,) ndarray
1077
            Apex base vector tangent to contours of constant V0
1078
        e2 : (3, N) or (3,) ndarray
1079
            Apex base vector that completes the right-handed system
1080
        e3 : (3, N) or (3,) ndarray
1081
            Apex base vector tangent to contours of constant PhiA
1082
1083
        Notes
1084
        -----
1085
        The vectors are described by Richmond [1995] [4]_ and
1086
        Emmert et al. [2010] [5]_.  The vector components are geodetic east,
1087
        north, and up (only east and north for `f1` and `f2`).
1088
1089
        `f3`, `g1`, `g2`, and `g3` are not part of the Fortran code
1090
        by Emmert et al. [2010] [5]_. They are calculated by this
1091
        Python library according to the following equations in
1092
        Richmond [1995] [4]_:
1093
1094
        * `g1`: Eqn. 6.3
1095
        * `g2`: Eqn. 6.4
1096
        * `g3`: Eqn. 6.5
1097
        * `f3`: Eqn. 6.8
1098
1099
        References
1100
        ----------
1101
        .. [4] Richmond, A. D. (1995), Ionospheric Electrodynamics Using
1102
               Magnetic Apex Coordinates, Journal of geomagnetism and
1103
               geoelectricity, 47(2), 191–212, :doi:`10.5636/jgg.47.191`.
1104
1105
        .. [5] Emmert, J. T., A. D. Richmond, and D. P. Drob (2010),
1106
               A computationally compact representation of Magnetic-Apex
1107
               and Quasi-Dipole coordinates with smooth base vectors,
1108
               J. Geophys. Res., 115(A8), A08322, doi:10.1029/2010JA015326.
1109
1110
        """
1111
        # Convert to geodetic coordinates from current coordinate system
1112
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
1113
                                  precision=precision)
1114
1115
        # Retrieve the desired magnetic locations and base vectors
1116
        returnvals = list(self._geo2apexall(glat, glon, height))
1117
        qlat = np.float64(returnvals[0])
1118
        alat = np.float64(returnvals[2])
1119
        bvec_ind = [4, 5, 7, 8, 9, 11, 12, 13]
1120
1121
        # If inputs are not scalar, each vector is an array of arrays,
1122
        # so reshape to a single array
1123
        if returnvals[4].dtype == object:
1124
            for i in bvec_ind:
1125
                returnvals[i] = np.vstack(returnvals[i]).T
1126
1127
        # Make sure the base vector arrays are 2D
1128
        for i in bvec_ind:
1129
            if i in [4, 5]:
1130
                rsize = (2, returnvals[i].size // 2)
1131
            else:
1132
                rsize = (3, returnvals[i].size // 3)
1133
            returnvals[i] = returnvals[i].reshape(rsize)
1134
1135
        # Assign the reshaped base vectors
1136
        f1, f2 = returnvals[4:6]
1137
        d1, d2, d3 = returnvals[7:10]
1138
        e1, e2, e3 = returnvals[11:14]
1139
1140
        # Compute f3, g1, g2, g3 (outstanding quasi-dipole base vectors)
1141
        #
1142
        # Start by calculating the D equivalent, F (F_scalar)
1143
        f1_stack = np.vstack((f1, np.zeros_like(f1[0])))
1144
        f2_stack = np.vstack((f2, np.zeros_like(f2[0])))
1145
        F_scalar = np.cross(f1_stack.T, f2_stack.T).T[-1]
1146
1147
        # Get the cosine of the magnetic inclination
1148
        cos_mag_inc = helpers.getcosIm(alat)
1149
1150
        # Define the k base vectors
1151
        k_unit = np.array([0, 0, 1], dtype=np.float64).reshape((3, 1))
1152
1153
        # Calculate the remaining quasi-dipole base vectors
1154
        g1 = ((self.RE + np.asarray(height))
1155
              / (self.RE + self.refh)) ** (3 / 2) * d1 / F_scalar
1156
        g2 = -1.0 / (2.0 * F_scalar * np.tan(np.radians(qlat))) * (
1157
            k_unit + ((self.RE + np.asarray(height))
1158
                      / (self.RE + self.refh)) * d2 / cos_mag_inc)
1159
        g3 = k_unit * F_scalar
1160
        f3 = np.cross(g1.T, g2.T).T
1161
1162
        # Reshape the output
1163
        out = [f1, f2, f3, g1, g2, g3, d1, d2, d3, e1, e2, e3]
1164
1165
        if np.any(alat == -9999):
1166
            warnings.warn(''.join(['Base vectors g, d, e, and f3 set to NaN ',
1167
                                   'where apex latitude is undefined (apex ',
1168
                                   'height may be < reference height)']))
1169
            mask = alat == -9999
1170
            for i in range(len(out) - 2):
1171
                out[i + 2] = np.where(mask, np.nan, out[i + 2])
1172
1173
        out = tuple(np.squeeze(bvec) for bvec in out)
1174
1175
        return out
1176
1177
    def get_apex(self, lat, height=None):
1178
        """Calculate the apex height along a field line.
1179
1180
        Parameters
1181
        ----------
1182
        lat : float
1183
            Apex latitude in degrees
1184
        height : float or NoneType
1185
            Height above the surface of the Earth in km or NoneType to use
1186
            reference height (default=None)
1187
1188
        Returns
1189
        -------
1190
        apex_height : float
1191
            Height of the field line apex in km
1192
1193
        """
1194
        # Check the latitude
1195
        lat = helpers.checklat(lat, name='alat')
1196
1197
        # Ensure the height is set
1198
        if height is None:
1199
            height = self.refh
1200
1201
        # Calculate the apex height
1202
        cos_lat_squared = np.cos(np.radians(lat)) ** 2
1203
        apex_height = (self.RE + height) / cos_lat_squared - self.RE
1204
1205
        return apex_height
1206
1207
    def get_height(self, lat, apex_height):
1208
        """Calculate the height given an apex latitude and apex height.
1209
1210
        Parameters
1211
        ----------
1212
        lat : float
1213
            Apex latitude in degrees
1214
        apex_height : float
1215
            Maximum height of the apex field line above the surface of the
1216
            Earth in km
1217
1218
        Returns
1219
        -------
1220
        height : float
1221
            Height above the surface of the Earth in km
1222
1223
        """
1224
        # Check the latitude
1225
        lat = helpers.checklat(lat, name='alat')
1226
1227
        # Calculate the height from the apex height
1228
        cos_lat_squared = np.cos(np.radians(lat)) ** 2
1229
        height = cos_lat_squared * (apex_height + self.RE) - self.RE
1230
1231
        return height
1232
1233
    def set_epoch(self, year):
1234
        """Updates the epoch for all subsequent conversions.
1235
1236
        Parameters
1237
        ----------
1238
        year : float
1239
            Decimal year
1240
1241
        """
1242
        # Set the year and load the data file
1243
        self.year = np.float64(year)
1244
        fa.loadapxsh(self.datafile, self.year)
1245
1246
        # Call the Fortran routine to set time
1247
        fa.cofrm(self.year, self.igrf_fn)
1248
        return
1249
1250
    def set_refh(self, refh):
1251
        """Updates the apex reference height for all subsequent conversions.
1252
1253
        Parameters
1254
        ----------
1255
        refh : float
1256
            Apex reference height in km
1257
1258
        Notes
1259
        -----
1260
        The reference height is the height to which field lines will be mapped,
1261
        and is only relevant for conversions involving apex (not quasi-dipole).
1262
1263
        """
1264
        self.refh = refh
1265
1266
    def get_babs(self, glat, glon, height):
1267
        """Returns the magnitude of the IGRF magnetic field in tesla.
1268
1269
        Parameters
1270
        ----------
1271
        glat : array_like
1272
            Geodetic latitude in degrees
1273
        glon : array_like
1274
            Geodetic longitude in degrees
1275
        height : array_like
1276
            Altitude in km
1277
1278
        Returns
1279
        -------
1280
        babs : ndarray or float
1281
            Magnitude of the IGRF magnetic field in Tesla
1282
1283
        """
1284
        # Get the absolute value of the magnetic field at the desired location
1285
        babs = self._get_babs(glat, glon, height)
1286
1287
        # If array is returned, the dtype is object, so convert to float
1288
        babs = helpers.set_array_float(babs)
1289
1290
        return babs
1291
1292
    def bvectors_apex(self, lat, lon, height, coords='geo', precision=1e-10):
1293
        """Returns the magnetic field vectors in apex coordinates.
1294
1295
        Parameters
1296
        ----------
1297
        lat : (N,) array_like or float
1298
            Latitude
1299
        lon : (N,) array_like or float
1300
            Longitude
1301
        height : (N,) array_like or float
1302
            Altitude in km
1303
        coords : {'geo', 'apex', 'qd'}, optional
1304
            Input coordinate system
1305
        precision : float, optional
1306
            Precision of output (degrees) when converting to geo. A negative
1307
            value of this argument produces a low-precision calculation of
1308
            geodetic lat/lon based only on their spherical harmonic
1309
            representation.
1310
            A positive value causes the underlying Fortran routine to iterate
1311
            until feeding the output geo lat/lon into geo2qd (APXG2Q) reproduces
1312
            the input QD lat/lon to within the specified precision (all
1313
            coordinates being converted to geo are converted to QD first and
1314
            passed through APXG2Q).
1315
1316
        Returns
1317
        -------
1318
        main_mag_e3: (1, N) or (1,) ndarray
1319
           IGRF magnitude divided by a scaling factor, D (d_scale) to give
1320
           the main B field magnitude along the e3 base vector
1321
        e3 : (3, N) or (3,) ndarray
1322
           Base vector tangent to the contours of constant V_0 and Phi_A
1323
        main_mag_d3: (1, N) or (1,) ndarray
1324
           IGRF magnitude multiplied by a scaling factor, D (d_scale) to give
1325
           the main B field magnitudee along the d3 base vector
1326
        d3 : (3, N) or (3,) ndarray
1327
           Base vector equivalent to the scaled main field unit vector
1328
1329
        Notes
1330
        -----
1331
        See Richmond, A. D. (1995) [4]_ equations 3.8-3.14
1332
1333
        The apex magnetic field vectors described by Richmond [1995] [4]_ and
1334
        Emmert et al. [2010] [5]_, specfically the Be3 (main_mag_e3) and Bd3
1335
        (main_mag_d3) components. The vector components are geodetic east,
1336
        north, and up.
1337
1338
        References
1339
        ----------
1340
        Richmond, A. D. (1995) [4]_
1341
        Emmert, J. T. et al. (2010) [5]_
1342
1343
        """
1344
        # Convert the current coordinates to geodetic coordinates
1345
        glat, glon = self.convert(lat, lon, coords, 'geo', height=height,
1346
                                  precision=precision)
1347
1348
        # Get the magnitude of the magnetic field at the desired location
1349
        babs = self.get_babs(glat, glon, height)
1350
1351
        # Retrieve the necessary base vectors
1352
        _, _, _, _, _, _, d1, d2, d3, _, _, e3 = self.basevectors_apex(
1353
            glat, glon, height, coords='geo')
1354
1355
        # Perform the calculations described in [4]
1356
        d1_cross_d2 = np.cross(d1.T, d2.T).T
1357
        d_scale = np.sqrt(np.sum(d1_cross_d2 ** 2, axis=0))  # D in [4] 3.13
1358
1359
        main_mag_e3 = babs / d_scale  # Solve for b0 in [4] 3.13
1360
        main_mag_d3 = babs * d_scale  # Solve for b0 in [4] 3.10
1361
1362
        return main_mag_e3, e3, main_mag_d3, d3
1363