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