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 |