| Total Complexity | 93 |
| Total Lines | 1335 |
| Duplicated Lines | 6.97 % |
| 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 | """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 | |||
| 9 | from apexpy import helpers |
||
| 10 | |||
| 11 | # Below try..catch required for autodoc to work on readthedocs |
||
| 12 | try: |
||
| 13 | from apexpy import fortranapex as fa |
||
| 14 | except ImportError as ierr: |
||
| 15 | warnings.warn("".join(["fortranapex module could not be imported, so ", |
||
| 16 | "apexpy probably won't work. Make sure you have ", |
||
| 17 | "a gfortran compiler. Failed with error: ", |
||
| 18 | str(ierr)])) |
||
| 19 | |||
| 20 | # Make sure invalid warnings are always shown |
||
| 21 | warnings.filterwarnings('always', message='.*set to NaN where*', |
||
| 22 | module='apexpy.apex') |
||
| 23 | |||
| 24 | |||
| 25 | class ApexHeightError(ValueError): |
||
| 26 | """Specialized ValueError definition, to be used when apex height is wrong. |
||
| 27 | """ |
||
| 28 | pass |
||
| 29 | |||
| 30 | |||
| 31 | class Apex(object): |
||
| 32 | """Calculates coordinate conversions, field-line mapping, and base vectors. |
||
| 33 | |||
| 34 | Parameters |
||
| 35 | ---------- |
||
| 36 | date : NoneType, float, :class:`dt.date`, or :class:`dt.datetime`, optional |
||
| 37 | Determines which IGRF coefficients are used in conversions. Uses |
||
| 38 | current date as default. If float, use decimal year. If None, uses |
||
| 39 | current UTC. (default=None) |
||
| 40 | refh : float, optional |
||
| 41 | Reference height in km for apex coordinates, the field lines are mapped |
||
| 42 | to this height. (default=0) |
||
| 43 | datafile : str or NoneType, optional |
||
| 44 | Path to custom coefficient file, if None uses `apexsh.dat` file |
||
| 45 | (default=None) |
||
| 46 | fortranlib : str or NoneType, optional |
||
| 47 | Path to Fortran Apex CPython library, if None uses linked library file |
||
| 48 | (default=None) |
||
| 49 | |||
| 50 | Attributes |
||
| 51 | ---------- |
||
| 52 | year : float |
||
| 53 | Decimal year used for the IGRF model |
||
| 54 | RE : float |
||
| 55 | Earth radius in km, defaults to mean Earth radius |
||
| 56 | refh : float |
||
| 57 | Reference height in km for apex coordinates |
||
| 58 | datafile : str |
||
| 59 | Path to coefficient file |
||
| 60 | fortranlib : str |
||
| 61 | Path to Fortran Apex CPython library |
||
| 62 | igrf_fn : str |
||
| 63 | IGRF coefficient filename |
||
| 64 | |||
| 65 | Notes |
||
| 66 | ----- |
||
| 67 | The calculations use IGRF-13 with coefficients from 1900 to 2025 [1]_. |
||
| 68 | |||
| 69 | The geodetic reference ellipsoid is WGS84. |
||
| 70 | |||
| 71 | References |
||
| 72 | ---------- |
||
| 73 | |||
| 74 | .. [1] Thébault, E. et al. (2015), International Geomagnetic Reference |
||
| 75 | Field: the 12th generation, Earth, Planets and Space, 67(1), 79, |
||
| 76 | :doi:`10.1186/s40623-015-0228-9`. |
||
| 77 | |||
| 78 | """ |
||
| 79 | |||
| 80 | # ------------------------ |
||
| 81 | # Define the magic methods |
||
| 82 | |||
| 83 | def __init__(self, date=None, refh=0, datafile=None, fortranlib=None): |
||
| 84 | |||
| 85 | if datafile is None: |
||
| 86 | datafile = os.path.join(os.path.dirname(__file__), 'apexsh.dat') |
||
| 87 | |||
| 88 | if fortranlib is None: |
||
| 89 | fortranlib = fa.__file__ |
||
| 90 | |||
| 91 | self.RE = 6371.009 # Mean Earth radius in km |
||
| 92 | self.set_refh(refh) # Reference height in km |
||
| 93 | |||
| 94 | if date is None: |
||
| 95 | self.year = helpers.toYearFraction(dt.datetime.utcnow()) |
||
| 96 | else: |
||
| 97 | try: |
||
| 98 | # Convert date/datetime object to decimal year |
||
| 99 | self.year = helpers.toYearFraction(date) |
||
| 100 | except AttributeError: |
||
| 101 | # Failed while finding datetime attribute, so |
||
| 102 | # date is probably an int or float; use directly |
||
| 103 | self.year = date |
||
| 104 | |||
| 105 | if not os.path.isfile(datafile): |
||
| 106 | raise IOError('Data file does not exist: {}'.format(datafile)) |
||
| 107 | |||
| 108 | if not os.path.isfile(fortranlib): |
||
| 109 | raise IOError('Fortran library does not exist: {}'.format( |
||
| 110 | fortranlib)) |
||
| 111 | |||
| 112 | self.datafile = datafile |
||
| 113 | self.fortranlib = fortranlib |
||
| 114 | |||
| 115 | # Set the IGRF coefficient text file name |
||
| 116 | self.igrf_fn = os.path.join(os.path.dirname(__file__), |
||
| 117 | 'igrf13coeffs.txt') |
||
| 118 | if not os.path.exists(self.igrf_fn): |
||
| 119 | raise OSError("File {} does not exist".format(self.igrf_fn)) |
||
| 120 | |||
| 121 | # Update the Fortran epoch using the year defined above |
||
| 122 | self.set_epoch(self.year) |
||
| 123 | |||
| 124 | # Vectorize the fortran functions |
||
| 125 | self._geo2qd = np.frompyfunc( |
||
| 126 | lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180, |
||
| 127 | height, 0)[:2], 3, 2) |
||
| 128 | self._geo2apex = np.frompyfunc( |
||
| 129 | lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360 |
||
| 130 | - 180, height, self.refh, |
||
| 131 | 0)[2:4], 3, 2) |
||
| 132 | self._geo2apexall = np.frompyfunc( |
||
| 133 | lambda glat, glon, height: fa.apxg2all(glat, (glon + 180) % 360 |
||
| 134 | - 180, height, self.refh, |
||
| 135 | 1), 3, 14) |
||
| 136 | self._qd2geo = np.frompyfunc( |
||
| 137 | lambda qlat, qlon, height, precision: fa.apxq2g(qlat, (qlon + 180) |
||
| 138 | % 360 - 180, height, |
||
| 139 | precision), 4, 3) |
||
| 140 | self._basevec = np.frompyfunc( |
||
| 141 | lambda glat, glon, height: fa.apxg2q(glat, (glon + 180) % 360 - 180, |
||
| 142 | height, 1)[2:4], 3, 2) |
||
| 143 | |||
| 144 | # Vectorize other nonvectorized functions |
||
| 145 | self._apex2qd = np.frompyfunc(self._apex2qd_nonvectorized, 3, 2) |
||
| 146 | self._qd2apex = np.frompyfunc(self._qd2apex_nonvectorized, 3, 2) |
||
| 147 | self._get_babs = np.frompyfunc(self._get_babs_nonvectorized, 3, 1) |
||
| 148 | |||
| 149 | return |
||
| 150 | |||
| 151 | def __repr__(self): |
||
| 152 | """Produce an evaluatable representation of the Apex class.""" |
||
| 153 | rstr = "".join(["apexpy.Apex(date=", self.year.__repr__(), ", refh=", |
||
| 154 | self.refh.__repr__(), ", datafile=", |
||
| 155 | self.datafile.__repr__(), ", fortranlib=", |
||
| 156 | self.fortranlib.__repr__(), ")"]) |
||
| 157 | |||
| 158 | return rstr |
||
| 159 | |||
| 160 | def __str__(self): |
||
| 161 | """Produce a user-friendly representation of the Apex class.""" |
||
| 162 | |||
| 163 | out_str = '\n'.join(["Apex class conversions performed with", |
||
| 164 | "-------------------------------------", |
||
| 165 | "Decimal year: {:.8f}".format(self.year), |
||
| 166 | "Reference height: {:.3f} km".format(self.refh), |
||
| 167 | "Earth radius: {:.3f} km".format(self.RE), '', |
||
| 168 | "Coefficient file: '{:s}'".format(self.datafile), |
||
| 169 | "Cython Fortran library: '{:s}'".format( |
||
| 170 | self.fortranlib)]) |
||
| 171 | return out_str |
||
| 172 | |||
| 173 | def __eq__(self, comp_obj): |
||
| 174 | """Performs equivalency evaluation. |
||
| 175 | |||
| 176 | Parameters |
||
| 177 | ---------- |
||
| 178 | comp_obj |
||
| 179 | Object of any time to be compared to the class object |
||
| 180 | |||
| 181 | Returns |
||
| 182 | ------- |
||
| 183 | bool or NotImplemented |
||
| 184 | True if self and comp_obj are identical, False if they are not, |
||
| 185 | and NotImplemented if the classes are not the same |
||
| 186 | |||
| 187 | """ |
||
| 188 | |||
| 189 | if isinstance(comp_obj, self.__class__): |
||
| 190 | # Objects are the same if all the attributes are the same |
||
| 191 | for apex_attr in ['year', 'refh', 'RE', 'datafile', 'fortranlib', |
||
| 192 | 'igrf_fn']: |
||
| 193 | bad_attr = False |
||
| 194 | if hasattr(self, apex_attr): |
||
| 195 | aval = getattr(self, apex_attr) |
||
| 196 | |||
| 197 | if hasattr(comp_obj, apex_attr): |
||
| 198 | cval = getattr(comp_obj, apex_attr) |
||
| 199 | |||
| 200 | if cval != aval: |
||
| 201 | # Not equal, as the attribute values differ |
||
| 202 | bad_attr = True |
||
| 203 | else: |
||
| 204 | # The comparison object is missing an attribute |
||
| 205 | bad_attr = True |
||
| 206 | else: |
||
| 207 | if hasattr(comp_obj, apex_attr): |
||
| 208 | # The current object is missing an attribute |
||
| 209 | bad_attr = True |
||
| 210 | |||
| 211 | if bad_attr: |
||
| 212 | return False |
||
| 213 | |||
| 214 | # If we arrive here, all expected attributes exist in both classes |
||
| 215 | # and have the same values |
||
| 216 | return True |
||
| 217 | |||
| 218 | return NotImplemented |
||
| 219 | |||
| 220 | # ------------------------- |
||
| 221 | # Define the hidden methods |
||
| 222 | |||
| 223 | View Code Duplication | def _apex2qd_nonvectorized(self, alat, alon, height): |
|
|
|
|||
| 224 | """Convert from apex to quasi-dipole (not-vectorised) |
||
| 225 | |||
| 226 | Parameters |
||
| 227 | ----------- |
||
| 228 | alat : (float) |
||
| 229 | Apex latitude in degrees |
||
| 230 | alon : (float) |
||
| 231 | Apex longitude in degrees |
||
| 232 | height : (float) |
||
| 233 | Height in km |
||
| 234 | |||
| 235 | Returns |
||
| 236 | --------- |
||
| 237 | qlat : (float) |
||
| 238 | Quasi-dipole latitude in degrees |
||
| 239 | qlon : (float) |
||
| 240 | Quasi-diplole longitude in degrees |
||
| 241 | |||
| 242 | """ |
||
| 243 | # Evaluate the latitude |
||
| 244 | alat = helpers.checklat(alat, name='alat') |
||
| 245 | |||
| 246 | # Convert modified apex to quasi-dipole, longitude is the same |
||
| 247 | qlon = alon |
||
| 248 | |||
| 249 | # Get the apex height |
||
| 250 | h_apex = self.get_apex(alat) |
||
| 251 | |||
| 252 | if h_apex < height: |
||
| 253 | if np.isclose(h_apex, height, rtol=0, atol=1e-5): |
||
| 254 | # Allow for values that are close |
||
| 255 | h_apex = height |
||
| 256 | else: |
||
| 257 | estr = ''.join(['height {:.3g} is > '.format(np.max(height)), |
||
| 258 | 'apex height {:.3g} for alat {:.3g}'.format( |
||
| 259 | h_apex, alat)]) |
||
| 260 | raise ApexHeightError(estr) |
||
| 261 | |||
| 262 | # Convert the latitude |
||
| 263 | salat = np.sign(alat) if alat != 0 else 1 |
||
| 264 | qlat = salat * np.degrees(np.arccos(np.sqrt((self.RE + height) |
||
| 265 | / (self.RE + h_apex)))) |
||
| 266 | |||
| 267 | return qlat, qlon |
||
| 268 | |||
| 269 | View Code Duplication | def _qd2apex_nonvectorized(self, qlat, qlon, height): |
|
| 270 | """Converts quasi-dipole to modified apex coordinates. |
||
| 271 | |||
| 272 | Parameters |
||
| 273 | ---------- |
||
| 274 | qlat : float |
||
| 275 | Quasi-dipole latitude |
||
| 276 | qlon : float |
||
| 277 | Quasi-dipole longitude |
||
| 278 | height : float |
||
| 279 | Altitude in km |
||
| 280 | |||
| 281 | Returns |
||
| 282 | ------- |
||
| 283 | alat : float |
||
| 284 | Modified apex latitude |
||
| 285 | alon : float |
||
| 286 | Modified apex longitude |
||
| 287 | |||
| 288 | Raises |
||
| 289 | ------ |
||
| 290 | ApexHeightError |
||
| 291 | if apex height < reference height |
||
| 292 | |||
| 293 | """ |
||
| 294 | # Evaluate the latitude |
||
| 295 | qlat = helpers.checklat(qlat, name='qlat') |
||
| 296 | |||
| 297 | # Get the longitude and apex height |
||
| 298 | alon = qlon |
||
| 299 | h_apex = self.get_apex(qlat, height) |
||
| 300 | |||
| 301 | if h_apex < self.refh: |
||
| 302 | if np.isclose(h_apex, self.refh, rtol=0, atol=1e-5): |
||
| 303 | # Allow for values that are close |
||
| 304 | h_apex = self.refh |
||
| 305 | else: |
||
| 306 | estr = ''.join(['apex height ({:.3g}) is < '.format(h_apex), |
||
| 307 | 'reference height ({:.3g})'.format(self.refh), |
||
| 308 | ' for qlat {:.3g}'.format(qlat)]) |
||
| 309 | raise ApexHeightError(estr) |
||
| 310 | |||
| 311 | # Convert the latitude |
||
| 312 | sqlat = np.sign(qlat) if qlat != 0 else 1 |
||
| 313 | alat = sqlat * np.degrees(np.arccos(np.sqrt((self.RE + self.refh) |
||
| 314 | / (self.RE + h_apex)))) |
||
| 315 | |||
| 316 | return alat, alon |
||
| 317 | |||
| 318 | def _map_EV_to_height(self, alat, alon, height, newheight, data, ev_flag): |
||
| 319 | """Maps electric field related values to a desired height |
||
| 320 | |||
| 321 | Parameters |
||
| 322 | ---------- |
||
| 323 | alat : array-like |
||
| 324 | Apex latitude in degrees. |
||
| 325 | alon : array-like |
||
| 326 | Apex longitude in degrees. |
||
| 327 | height : array-like |
||
| 328 | Current altitude in km. |
||
| 329 | new_height : array-like |
||
| 330 | Desired altitude to which EV values will be mapped in km. |
||
| 331 | data : array-like |
||
| 332 | 3D value(s) for the electric field or electric drift |
||
| 333 | ev_flag : str |
||
| 334 | Specify if value is an electric field ('E') or electric drift ('V') |
||
| 335 | |||
| 336 | Returns |
||
| 337 | ------- |
||
| 338 | data_mapped : array-like |
||
| 339 | Data mapped along the magnetic field from the old height to the |
||
| 340 | new height. |
||
| 341 | |||
| 342 | Raises |
||
| 343 | ------ |
||
| 344 | ValueError |
||
| 345 | If an unknown `ev_flag` or badly shaped data input is supplied. |
||
| 346 | |||
| 347 | """ |
||
| 348 | # Ensure the E-V flag is correct |
||
| 349 | ev_flag = ev_flag.upper() |
||
| 350 | if ev_flag not in ['E', 'V']: |
||
| 351 | raise ValueError('unknown electric field/drift flag') |
||
| 352 | |||
| 353 | # Make sure data is array of correct shape |
||
| 354 | if not (np.ndim(data) == 1 |
||
| 355 | and np.size(data) == 3) and not (np.ndim(data) == 2 |
||
| 356 | 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 |