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