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