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