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