geopack-vectorize 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
geopack/geopack.py ADDED
@@ -0,0 +1,1449 @@
1
+ import numpy as np
2
+ import os.path
3
+ import datetime
4
+ from urllib.request import urlopen, Request
5
+ from urllib.parse import urljoin
6
+ from fnmatch import fnmatch
7
+
8
+ # Import models
9
+ from .models import t89, t96, t01, t04
10
+
11
+ igrf_pattern = 'igrf*coeffs.txt'
12
+
13
+ def update_igrf(local_dir):
14
+ """
15
+ Update to the latest IGRF coefficients.
16
+ """
17
+
18
+ # Where to find the files.
19
+ url = 'http://www.ngdc.noaa.gov/IAGA/vmod/coeffs/'
20
+
21
+ # Find the coeff files.
22
+ coef_files = []
23
+ try:
24
+ request = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
25
+ with urlopen(request) as response:
26
+ if response.status != 200:
27
+ print('Failed to get the list of IGRF coefficients. Status code: {response.status}')
28
+ else:
29
+ html = response.read().decode('utf-8')
30
+ for line in html.splitlines():
31
+ if 'href' in line and '.txt' in line:
32
+ start = line.find('href="')+6
33
+ end = line.find('"', start)
34
+ base_name = line[start:end]
35
+ if fnmatch(base_name, igrf_pattern):
36
+ coef_files.append(base_name)
37
+ except:
38
+ print('Failed to get the list of IGRF coefficients.')
39
+
40
+ # Download the files.
41
+ for coef_file in coef_files:
42
+ local_file = os.path.join(local_dir, coef_file)
43
+ if os.path.exists(local_file): continue
44
+
45
+ remote_file = urljoin(url, coef_file)
46
+ with urlopen(remote_file) as response:
47
+ if response.status != 200: continue
48
+ with open(local_file, 'wb') as file:
49
+ file.write(response.read())
50
+
51
+
52
+
53
+
54
+
55
+ def init_igrf(version=None):
56
+ """
57
+ Initialize the IGRF coefficients and related coefs.
58
+ Should be called once and only once when importing the geopack module.
59
+
60
+ :param version: The version of IGRF coefficients to load, e.g., '13'. If None, the latest version will be loaded.
61
+ """
62
+
63
+ global igrf, nmn,mns, nyear,years,yruts
64
+
65
+ print('Load IGRF coefficients ...')
66
+
67
+ # Load all available IGRF coefficients.
68
+ local_dir = os.path.join(os.path.dirname(__file__), 'igrf_coeffs')
69
+ if not os.path.exists(local_dir): os.mkdir(local_dir)
70
+ update_igrf(local_dir)
71
+
72
+ if version is None:
73
+ # Load the latest version.
74
+ coef_files = os.listdir(local_dir)
75
+ versions = []
76
+ prefix, suffix = igrf_pattern.split('*')
77
+ for f in coef_files:
78
+ if f.startswith(prefix) and f.endswith(suffix):
79
+ versions.append(f[len(prefix):-len(suffix)])
80
+ version = max(versions, key=int)
81
+
82
+ base_name = 'igrf'+version+'coeffs.txt'
83
+ coef_file = os.path.join(local_dir, base_name)
84
+
85
+ nheader = 3
86
+ with open(coef_file, 'r') as file:
87
+ for i in range(nheader):
88
+ next(file)
89
+ header = file.readline().rstrip()
90
+ cols = header.split()
91
+ # first 3 columns are g/h flag, n, m.
92
+ years = np.array([np.int32(j[0:4]) for j in cols[3:]])
93
+ nyear = len(years)
94
+ lines = file.read().splitlines()
95
+
96
+ cols = lines[-1].split()
97
+ k = np.int32(cols[1]) + 1
98
+ nmn = np.int32((k + 1) * k * 0.5)
99
+ igrf = np.zeros((nmn, nyear, 2), dtype=float)
100
+ mns = np.empty((nmn, 2), dtype=np.int32)
101
+ l = 0
102
+ for i in range(k):
103
+ for j in range(i + 1):
104
+ mns[l, :] = [i, j]
105
+ l += 1
106
+
107
+ for line in lines:
108
+ cols = line.split()
109
+ if cols[0] == 'g':
110
+ i = 0
111
+ else:
112
+ i = 1
113
+ n, m = np.int32(cols[1:3])
114
+ mn = np.int32(n * (n + 1) * 0.5 + m)
115
+ igrf[mn, :, i] = [np.float32(j) for j in cols[3:]]
116
+
117
+ # treat the last column
118
+ years[-1] += 5
119
+ igrf[:, -1, :] = igrf[:, -2, :] + igrf[:, -1, :] * 5
120
+ yruts = np.empty(nyear, dtype=float)
121
+ t0_datetime = datetime.datetime(1970,1,1)
122
+ for i in range(nyear):
123
+ yruts[i] = (datetime.datetime(years[i],1,1)-t0_datetime).total_seconds()
124
+ # separate g/h
125
+ igrf = {'g': igrf[:,:,0], 'h': igrf[:,:,1]}
126
+
127
+
128
+ def load_igrf(ut):
129
+ """
130
+ Load the IGRF coefficients for the given time.
131
+ :param ut: ut sec, a float.
132
+ :return: g,h. The IGRF coef at given time.
133
+ """
134
+
135
+ # igrf should be initilized already.
136
+ global igrf, nmn,nyear, mns, years, yruts
137
+ try: isinstance(igrf, dict)
138
+ except: init_igrf()
139
+
140
+ # locate the two years of interest.
141
+ if ut <= yruts[0]: yridx = 0
142
+ elif ut >= yruts[-1]: yridx = -2
143
+ else:
144
+ yridx = np.argwhere(yruts <= ut)[-1]
145
+
146
+ g0 = np.squeeze(igrf['g'][:,yridx])
147
+ h0 = np.squeeze(igrf['h'][:,yridx])
148
+ g1 = np.squeeze(igrf['g'][:,yridx+1])
149
+ h1 = np.squeeze(igrf['h'][:,yridx+1])
150
+
151
+ ut0 = yruts[yridx]
152
+ ut1 = yruts[yridx+1]
153
+ f1 = (ut-ut0)/(ut1-ut0)
154
+ f0 = 1-f1
155
+
156
+ return g0*f0+g1*f1, h0*f0+h1*f1
157
+
158
+
159
+ def igrf_gsw(xgsw,ygsw,zgsw):
160
+ """
161
+ Calculates components of the main (internal) geomagnetic field in the geocentric solar
162
+ magnetospheric coordinate system, using IAGA international geomagnetic reference model
163
+ coefficients (e.g., http://www.ngdc.noaa.gov/iaga/vmod/igrf.html revised: 22 march, 2005)
164
+
165
+ Before the first call of this subroutine, or if the date/time
166
+ was changed, the model coefficients and GEO-GSW rotation matrix elements should be updated
167
+ by calling the subroutine recalc
168
+
169
+ Python version by Sheng Tian
170
+
171
+ :param xgsw,ygsw,zgsw: cartesian GSW coordinates (in units Re=6371.2 km)
172
+ :return: hxgsw,hygsw,hzgsw. Cartesian GSW components of the main geomagnetic field in nanotesla
173
+ """
174
+ xgsm,ygsm,zgsm = gswgsm(xgsw,ygsw,zgsw, 1)
175
+ bxgsm,bygsm,bzgsm = igrf_gsm(xgsm,ygsm,zgsm)
176
+ return gswgsm(bxgsm,bygsm,bzgsm, -1)
177
+
178
+
179
+ def igrf_gsm(xgsm,ygsm,zgsm):
180
+ """
181
+ Calculates components of the main (internal) geomagnetic field in the geocentric solar
182
+ magnetospheric coordinate system, using IAGA international geomagnetic reference model
183
+ coefficients (e.g., http://www.ngdc.noaa.gov/iaga/vmod/igrf.html revised: 22 march, 2005)
184
+
185
+ Before the first call of this subroutine, or if the date/time
186
+ was changed, the model coefficients and GEO-GSM rotation matrix elements should be updated
187
+ by calling the subroutine recalc
188
+
189
+ Python version by Sheng Tian
190
+
191
+ :param xgsm,ygsm,zgsm: cartesian GSM coordinates (in units Re=6371.2 km)
192
+ :return: hxgsm,hygsm,hzgsm. Cartesian GSM components of the main geomagnetic field in nanotesla
193
+ """
194
+
195
+ xgeo,ygeo,zgeo = geogsm(xgsm,ygsm,zgsm, -1)
196
+ r,theta,phi = sphcar(xgeo,ygeo,zgeo, -1)
197
+ br,btheta,bphi = igrf_geo(r,theta,phi)
198
+ bxgeo,bygeo,bzgeo = bspcar(theta,phi,br,btheta,bphi)
199
+ return geogsm(bxgeo,bygeo,bzgeo, 1)
200
+
201
+
202
+ def igrf_geo(r,theta,phi):
203
+ """
204
+ Calculates components of the main (internal) geomagnetic field in the spherical geographic
205
+ (geocentric) coordinate system, using IAGA international geomagnetic reference model
206
+ coefficients (e.g., http://www.ngdc.noaa.gov/iaga/vmod/igrf.html, revised: 22 march, 2005)
207
+
208
+ Before the first call of this subroutine, or if the time was changed,
209
+ the model coefficients should be updated by calling the subroutine recalc
210
+
211
+ Python version by Sheng Tian
212
+
213
+ :param r: spherical geographic (geocentric) coordinates: radial distance r in units Re=6371.2 km
214
+ :param theta: colatitude theta in radians
215
+ :param phi: longitude phi in radians
216
+ :return: br, btheta, bphi. Spherical components of the main geomagnetic field in nanotesla
217
+ (positive br outward, btheta southward, bphi eastward)
218
+ """
219
+
220
+
221
+ # common /geopack2/ g(105),h(105),rec(105)
222
+ global g, h, rec
223
+
224
+ ct = np.cos(theta)
225
+ st = np.sin(theta)
226
+ minst = 1e-5
227
+ if np.abs(st) < minst: smlst = True
228
+ else: smlst = False
229
+
230
+ # In this new version, the optimal value of the parameter nm (maximal order of the spherical
231
+ # harmonic expansion) is not user-prescribed, but calculated inside the subroutine, based
232
+ # on the value of the radial distance r:
233
+ irp3 = np.int64(r+2)
234
+ nm = np.int64(3+30/irp3)
235
+ if nm > 13: nm = 13
236
+ k = nm+1
237
+
238
+ # r dependence is encapsulated here.
239
+ a = np.empty(k)
240
+ b = np.empty(k)
241
+ ar = 1/r # a/r
242
+ a[0] = ar*ar # a[n] = (a/r)^(n+2).
243
+ b[0] = a[0] # b[n] = (n+1)(a/r)^(n+2)
244
+ for n in range(1,k):
245
+ a[n] = a[n-1]*ar
246
+ b[n] = a[n]*(n+1)
247
+
248
+
249
+ # t - short for theta, f - short for phi.
250
+ br,bt,bf = [0.]*3
251
+ d,p = [0.,1]
252
+
253
+ # m = 0. P^n,0
254
+ m = 0
255
+ smf,cmf = [0.,1]
256
+ p1,d1,p2,d2 = [p,d,0.,0]
257
+ l0 = 0
258
+ mn = l0
259
+ for n in range(m,k):
260
+ w = g[mn]*cmf+h[mn]*smf
261
+ br += b[n]*w*p1 # p1 is P^n,m.
262
+ bt -= a[n]*w*d1 # d1 is dP^n,m/dt.
263
+ xk = rec[mn]
264
+ # Eq 16c and its derivative on theta.
265
+ d0 = ct*d1-st*p1-xk*d2 # dP^n,m/dt = ct*dP^n-1,m/dt - st*P_n-1,m - K^n,m*dP^n-2,m/dt
266
+ p0 = ct*p1-xk*p2 # P^n,m = ct*P^n-1,m - K^n,m*P^n-2,m
267
+ d2,p2,d1 = [d1,p1,d0]
268
+ p1 = p0
269
+ mn += n+1
270
+
271
+ # Eq 16b and its derivative on theta.
272
+ d = st*d+ct*p # dP^m,m/dt = st*dP^m-1,m-1/dt + ct*P^m-1,m-1
273
+ p = st*p # P^m,m = st*P^m-1,m-1
274
+
275
+ # Similarly for P^n,m
276
+ l0 = 0
277
+ for m in range(1,k): # sum over m
278
+ smf = np.sin(m*phi) # sin(m*phi)
279
+ cmf = np.cos(m*phi) # cos(m*phi)
280
+ p1,d1,p2,d2 = [p,d,0.,0]
281
+ tbf = 0.
282
+ l0 += m+1
283
+ mn = l0
284
+ for n in range(m,k): # sum over n
285
+ w=g[mn]*cmf+h[mn]*smf # [g^n,m*cos(m*phi)+h^n,m*sin(m*phi)]
286
+ br += b[n]*w*p1
287
+ bt -= a[n]*w*d1
288
+ tp = p1
289
+ if smlst: tp = d1
290
+ tbf += a[n]*(g[mn]*smf-h[mn]*cmf)*tp
291
+ xk = rec[mn]
292
+ d0 = ct*d1-st*p1-xk*d2 # dP^n,m/dt = ct*dP^n-1,m/dt - st*P_n-1,m - K^n,m*dP^n-2,m/dt
293
+ p0 = ct*p1-xk*p2 # P^n,m = ct*P^n-1,m - K^n,m*P^n-2,m
294
+ d2,p2,d1 = [d1,p1,d0]
295
+ p1=p0
296
+ mn += n+1
297
+
298
+ d = st*d+ct*p
299
+ p = st*p
300
+
301
+ # update B_phi.
302
+ tbf *= m
303
+ bf += tbf
304
+
305
+ if smlst:
306
+ if ct < 0.: bf = -bf
307
+ else: bf /= st
308
+
309
+ return br,bt,bf
310
+
311
+
312
+
313
+
314
+ def dip(xgsm,ygsm,zgsm):
315
+ """
316
+ Calculates gsm components of a geodipole field with the dipole moment
317
+ corresponding to the epoch, specified by calling subroutine recalc (should be
318
+ invoked before the first use of this one and in case the date/time was changed).
319
+
320
+ :param xgsm,ygsm,zgsm: GSM coordinates in Re (1 Re = 6371.2 km)
321
+ :return: bxgsm,bygsm,gzgsm. Field components in gsm system, in nanotesla.
322
+
323
+ Last modification: May 4, 2005.
324
+ Author: N. A. Tsyganenko
325
+ """
326
+
327
+ # common /geopack1/ aaa(10),sps,cps,bbb(23)
328
+ # common /geopack2/ g(105),h(105),rec(105)
329
+ global aaa, sps,cps, bbb, g, h, rec
330
+
331
+ dipmom = np.sqrt(g[1]**2+g[2]**2+h[2]**2)
332
+
333
+ p = xgsm**2
334
+ u = zgsm**2
335
+ v = 3*zgsm*xgsm
336
+ t = ygsm**2
337
+ q = dipmom/np.sqrt(p+t+u)**5
338
+
339
+ bxgsm = q*((t+u-2.*p)*sps-v*cps)
340
+ bygsm = -3.*ygsm*q*(xgsm*sps+zgsm*cps)
341
+ bzgsm = q*((p+t-2.*u)*cps-v*sps)
342
+
343
+ return bxgsm,bygsm,bzgsm
344
+
345
+
346
+ def dip_gsw(xgsw,ygsw,zgsw):
347
+ """
348
+ Calculates gsm components of a geodipole field with the dipole moment
349
+ corresponding to the epoch, specified by calling subroutine recalc (should be
350
+ invoked before the first use of this one and in case the date/time was changed).
351
+
352
+ :param xgsw,ygsw,zgsw: GSW coordinates in Re (1 Re = 6371.2 km)
353
+ :return: bxgsm,bygsm,gzgsm. Field components in gsm system, in nanotesla.
354
+
355
+ Author: Sheng Tian
356
+ """
357
+ xgsm,ygsm,zgsm = gswgsm(xgsw,ygsw,zgsw, 1)
358
+ bxgsm,bygsm,bzgsm = dip(xgsm,ygsm,zgsm)
359
+ return gswgsm(bxgsm,bygsm,bzgsm, -1)
360
+
361
+
362
+ def recalc(ut, vxgse=-400,vygse=0,vzgse=0):
363
+ """
364
+ 1. Prepares elements of rotation matrices for transformations of vectors between
365
+ several coordinate systems, most frequently used in space physics.
366
+ 2. Prepares coefficients used in the calculation of the main geomagnetic field (igrf model)
367
+
368
+ This subroutine should be invoked before using the following subroutines:
369
+ igrf_geo, igrf_gsm, dip, geomag, geogsm, magsm, smgsm, gsmgse, geigeo.
370
+ There is no need to repeatedly invoke recalc, if multiple calculations are made for the same date and time.
371
+
372
+ :param ut: Universal time in second.
373
+ :param v[xyz]gse: The solar wind velocity expressed in GSE.
374
+ :return: psi. Dipole tilt angle in radian.
375
+
376
+ Python version by Sheng Tian
377
+ """
378
+
379
+
380
+ # The common block /geopack1/ contains elements of the rotation matrices and other
381
+ # parameters related to the coordinate transformations performed by this package
382
+ # common /geopack1/ st0,ct0,sl0,cl0,ctcl,stcl,ctsl,stsl,sfi,cfi,sps,
383
+ # cps,shi,chi,hi,psi,xmut,a11,a21,a31,a12,a22,a32,a13,a23,a33,ds3,
384
+ # cgst,sgst,ba(6)
385
+
386
+ # st0/ct0 - sin/cos of teta0 (colat in geo?). sl0/cl0 - sin/cos of lambda0 (longitude in geo?).
387
+ # ctcl/stcl - . ctsl/stsl - . geo x and z?
388
+ # sfi/cfi/xmut - rotate angle between mag and sm and its sin/cos.
389
+ # sps/cps/psi - tilt angle and its sin/cos.
390
+ # shi/chi/hi - rotate angle between gse to gsm and its sin/cos.
391
+ # a[11,...,33] - matrix converts geo to gsm.
392
+ # cgst/sgst - cos/sin of gst.
393
+ # ds3.
394
+ # ba(6).
395
+ global st0,ct0,sl0,cl0,ctcl,stcl,ctsl,stsl,sfi,cfi,sps,cps, \
396
+ shi,chi,hi,psi,xmut,ds3,cgst,sgst,ba, \
397
+ a11,a21,a31,a12,a22,a32,a13,a23,a33, \
398
+ e11,e21,e31,e12,e22,e32,e13,e23,e33
399
+
400
+ # The common block /geopack2/ contains coefficients of the IGRF field model, calculated
401
+ # for a given year and day from their standard epoch values. the array rec contains
402
+ # coefficients used in the recursion relations for legendre associate polynomials.
403
+ # common /geopack2/ g(105),h(105),rec(105)
404
+ global g,h,rec
405
+
406
+
407
+ # Compute the m,n related coefficients (following the notation in Davis 2004):
408
+ # 1. The Schmidt quasi-normalization: S_n,m, which normalizes the associated Legendre polynomials P_n^m
409
+ # to the Guassian normalized associated Legendre polynomials P^n,m.
410
+ #
411
+ # Since g_n^m * P_n^m should be constant, mutiplying S_n,m to g_n^m is equivalently converting P_n^m to P^n,m.
412
+ # The benefit of doing so is that P^n,m follows simple recursive form, c.f. Eq (16 a-c) and Eq(17 a-b).
413
+ # 2. rec[mn], which used in the recursion relation.
414
+ k = 14
415
+ nmn = np.int32((k+1)*k/2)
416
+
417
+ # rec[mn].
418
+ rec = np.empty(nmn,dtype=float)
419
+ mn = 0
420
+ for n in range(k): # K^1,m = 0, Eq (17a), automatically done.
421
+ n2 = 2*n+1
422
+ n2 = n2*(n2-2)
423
+ for m in range(n+1):
424
+ rec[mn] = (n-m)*(n+m)/n2 # K^n,m = (n-m)(n+m)/(2n+1)(2n-1), Eq (17b)
425
+ mn += 1
426
+
427
+ # coefficients for a given time, g_n^m(t), h_n^m(t)
428
+ g,h = load_igrf(ut)
429
+
430
+ # now multiply them by schmidt normalization factors:
431
+ s = 1. # S_0,0 = 1, Eq (18a)
432
+ mn = 0
433
+ for n in range(1,k):
434
+ mn += 1 # skip m=0.
435
+ s *= (2*n-1)/n
436
+ g[mn] *= s # S_n,0 = S_n-1,0 * (2n-1)/n, Eq (18b)
437
+ h[mn] *= s
438
+ p = s
439
+ for m in range(1,n+1):
440
+ if m == 1: aa = 2 # aa = delta_m,1
441
+ else: aa = 1
442
+ p *= np.sqrt(aa*(n-m+1)/(n+m))
443
+ mn += 1
444
+ g[mn] *= p # S_n,m = S_n,m-1 * sqrt(aa(n-m+1)/(n+m)), Eq (18c)
445
+ h[mn] *= p # now g/h are actually g^n,m, Eq (14 a-b)
446
+
447
+ g10=-g[1]
448
+ g11=-g[2]
449
+ h11=-h[2]
450
+
451
+ # Now calculate the components of the unit vector ezmag in geo coord.system:
452
+ # sin(teta0)*cos(lambda0), sin(teta0)*sin(lambda0), and cos(teta0)
453
+ # st0 * cl0 st0 * sl0 ct0
454
+ sq=g11**2+h11**2
455
+ sqq=np.sqrt(sq)
456
+ sqr=np.sqrt(g10**2+sq)
457
+ sl0= h11/sqq
458
+ cl0= g11/sqq
459
+ st0= sqq/sqr
460
+ ct0= g10/sqr
461
+ stcl=st0*cl0
462
+ stsl=st0*sl0
463
+ ctsl=ct0*sl0
464
+ ctcl=ct0*cl0
465
+
466
+
467
+ gst,slong,srasn,sdec,obliq = sun(ut)
468
+
469
+ # All vectors are expressed in GEI.
470
+
471
+ # xgse_[xyz] (s[123]) are the components of the unit vector exgsm=exgse in GEI,
472
+ # pointing from the earth's center to the sun:
473
+ xgse_x=np.cos(srasn)*np.cos(sdec)
474
+ xgse_y=np.sin(srasn)*np.cos(sdec)
475
+ xgse_z=np.sin(sdec)
476
+
477
+ # zgse_[xyz] (dz[123]) in GEI has the components (0,-sin(delta),cos(delta)) = (0.,-0.397823,0.917462);
478
+ # Here delta = 23.44214 deg for the epoch 1978 (see the book by gurevich or other astronomical handbooks).
479
+ # Here the most accurate time-dependent formula is used:
480
+ zgse_x=0.
481
+ zgse_y=-np.sin(obliq)
482
+ zgse_z= np.cos(obliq)
483
+
484
+ # ygse_[xyz] (dy[123]) = zgse_[xyz] x xgsm_[xyz] in GEI:
485
+ ygse_x=zgse_y*xgse_z-zgse_z*xgse_y
486
+ ygse_y=zgse_z*xgse_x-zgse_x*xgse_z
487
+ ygse_z=zgse_x*xgse_y-zgse_y*xgse_x
488
+
489
+ # zsm_[xyz] (dip[123]) are the components of the unit vector zsm=zmag in GEI:
490
+ cgst=np.cos(gst)
491
+ sgst=np.sin(gst)
492
+ zsm_x=stcl*cgst-stsl*sgst
493
+ zsm_y=stcl*sgst+stsl*cgst
494
+ zsm_z=ct0
495
+
496
+ # xgsw_[xyz] (x[123]) in GEI.
497
+ v1 = -1/np.sqrt(vxgse*vxgse+vygse*vygse+vzgse*vzgse)
498
+ xgsw_x = (vxgse*xgse_x + vygse*ygse_x + vzgse*zgse_x)*v1
499
+ xgsw_y = (vxgse*xgse_y + vygse*ygse_y + vzgse*zgse_y)*v1
500
+ xgsw_z = (vxgse*xgse_z + vygse*ygse_z + vzgse*zgse_z)*v1
501
+
502
+ # ygsw (y[123]) = zsm x xgsw in GEI.
503
+ ygsw_x=zsm_y*xgsw_z-zsm_z*xgsw_y
504
+ ygsw_y=zsm_z*xgsw_x-zsm_x*xgsw_z
505
+ ygsw_z=zsm_x*xgsw_y-zsm_y*xgsw_x
506
+ y=np.sqrt(ygsw_x*ygsw_x+ygsw_y*ygsw_y+ygsw_z*ygsw_z)
507
+ ygsw_x=ygsw_x/y
508
+ ygsw_y=ygsw_y/y
509
+ ygsw_z=ygsw_z/y
510
+
511
+ # zgsw (z[123]) = xgsw x ygsw in GEI.
512
+ zgsw_x = xgsw_y*ygsw_z-xgsw_z*ygsw_y
513
+ zgsw_y = xgsw_z*ygsw_x-xgsw_x*ygsw_z
514
+ zgsw_z = xgsw_x*ygsw_y-xgsw_y*ygsw_x
515
+
516
+
517
+ # xgsm = xgse in GEI.
518
+ xgsm_x,xgsm_y,xgsm_z = xgse_x,xgse_y,xgse_z
519
+
520
+ # ygsm = zsm x xgsm in GEI.
521
+ ygsm_x = zsm_y*xgsm_z - zsm_z*xgsm_y
522
+ ygsm_y = zsm_z*xgsm_x - zsm_x*xgsm_z
523
+ ygsm_z = zsm_x*xgsm_y - zsm_y*xgsm_x
524
+ y=np.sqrt(ygsm_x*ygsm_x+ygsm_y*ygsm_y+ygsm_z*ygsm_z)
525
+ ygsm_x = ygsm_x/y
526
+ ygsm_y = ygsm_y/y
527
+ ygsm_z = ygsm_z/y
528
+
529
+ # ezgsm = exgsm x eygsm in GEI.
530
+ zgsm_x = xgse_y*ygsm_z-xgse_z*ygsm_y
531
+ zgsm_y = xgse_z*ygsm_x-xgse_x*ygsm_z
532
+ zgsm_z = xgse_x*ygsm_y-xgse_y*ygsm_x
533
+
534
+ # The elements of the matrix gse to gsm are the scalar products:
535
+ # chi=em22=(eygsm,eygse), shi=em23=(eygsm,ezgse), em32=(ezgsm,eygse)=-em23, and em33=(ezgsm,ezgse)=em22
536
+ chi = ygsm_x*ygse_x + ygsm_y*ygse_y + ygsm_z*ygse_z
537
+ shi = ygsm_x*zgse_x + ygsm_y*zgse_y + ygsm_z*zgse_z
538
+ hi = np.arcsin(shi)
539
+
540
+ # elements of the matrix gsm to gsw are the scalar products:
541
+ # e11 = (exgsm, exgsw) e12 = (exgsm, eygsw) e13 = (exgsm, ezgsw)
542
+ # e21 = (eygsm, exgsw) e22 = (eygsm, eygsw) e23 = (eygsm, ezgsw)
543
+ # e31 = (ezgsm, exgsw) e32 = (ezgsm, eygsw) e33 = (ezgsm, ezgsw)
544
+ e11 = xgsm_x*xgsw_x + xgsm_y*xgsw_y + xgsm_z*xgsw_z
545
+ e12 = xgsm_x*ygsw_x + xgsm_y*ygsw_y + xgsm_z*ygsw_z
546
+ e13 = xgsm_x*zgsw_x + xgsm_y*zgsw_y + xgsm_z*zgsw_z
547
+ e21 = ygsm_x*xgsw_x + ygsm_y*xgsw_y + ygsm_z*xgsw_z
548
+ e22 = ygsm_x*ygsw_x + ygsm_y*ygsw_y + ygsm_z*ygsw_z
549
+ e23 = ygsm_x*zgsw_x + ygsm_y*zgsw_y + ygsm_z*zgsw_z
550
+ e31 = zgsm_x*xgsw_x + zgsm_y*xgsw_y + zgsm_z*xgsw_z
551
+ e32 = zgsm_x*ygsw_x + zgsm_y*ygsw_y + zgsm_z*ygsw_z
552
+ e33 = zgsm_x*zgsw_x + zgsm_y*zgsw_y + zgsm_z*zgsw_z
553
+
554
+
555
+ # Tilt angle: psi=arcsin(ezsm dot exgsm)
556
+ sps=zsm_x*xgse_x+zsm_y*xgse_y+zsm_z*xgse_z
557
+ cps=np.sqrt(1.-sps**2)
558
+ psi=np.arcsin(sps)
559
+
560
+ # The elements of the matrix mag to sm are the scalar products:
561
+ # cfi=gm22=(eysm,eymag), sfi=gm23=(eysm,exmag); They can be derived as follows:
562
+ # In geo the vectors exmag and eymag have the components (ct0*cl0,ct0*sl0,-st0) and (-sl0,cl0,0), respectively.
563
+
564
+ # Hence, in gei the components are:
565
+ # exmag: ct0*cl0*cos(gst)-ct0*sl0*sin(gst)
566
+ # ct0*cl0*sin(gst)+ct0*sl0*cos(gst)
567
+ # -st0
568
+ # eymag: -sl0*cos(gst)-cl0*sin(gst)
569
+ # -sl0*sin(gst)+cl0*cos(gst)
570
+ # 0
571
+ # The components of eysm in gei were found above as ysm_in_geix, ysm_in_geiy, and ysm_in_geiz;
572
+ # Now we only have to combine the quantities into scalar products:
573
+ xmag_x= ct0*(cl0*cgst-sl0*sgst)
574
+ xmag_y= ct0*(cl0*sgst+sl0*cgst)
575
+ xmag_z=-st0
576
+ ymag_x=-(sl0*cgst+cl0*sgst)
577
+ ymag_y=-(sl0*sgst-cl0*cgst)
578
+ cfi=ygsm_x*ymag_x+ygsm_y*ymag_y
579
+ sfi=ygsm_x*xmag_x+ygsm_y*xmag_y+ygsm_z*xmag_z
580
+
581
+ xmut=(np.arctan2(sfi,cfi)+3.1415926536)*3.8197186342
582
+
583
+ # The elements of the matrix geo to gsm are the scalar products:
584
+ # a11=(exgeo,exgsm), a12=(eygeo,exgsm), a13=(ezgeo,exgsm),
585
+ # a21=(exgeo,eygsm), a22=(eygeo,eygsm), a23=(ezgeo,eygsm),
586
+ # a31=(exgeo,ezgsm), a32=(eygeo,ezgsm), a33=(ezgeo,ezgsm),
587
+ # All the unit vectors in brackets are already defined in gei:
588
+ # xgeo=(cgst,sgst,0), ygeo=(-sgst,cgst,0), zgeo=(0,0,1)
589
+ # and therefore:
590
+ a11= xgsm_x*cgst+xgse_y*sgst
591
+ a12=-xgsm_x*sgst+xgse_y*cgst
592
+ a13= xgsm_z
593
+ a21= ygsm_x *cgst+ygsm_y *sgst
594
+ a22=-ygsm_x *sgst+ygsm_y *cgst
595
+ a23= ygsm_z
596
+ a31= zgsm_x*cgst+zgsm_y*sgst
597
+ a32=-zgsm_x*sgst+zgsm_y*cgst
598
+ a33= zgsm_z
599
+
600
+ return psi
601
+
602
+ def sun(ut):
603
+ """
604
+ Calculates four quantities necessary for coordinate transformations
605
+ which depend on sun position (and, hence, on universal time and season)
606
+ Based on http://aa.usno.navy.mil/faq/docs/SunApprox.php and http://aa.usno.navy.mil/faq/docs/GAST.php
607
+
608
+ :param ut: ut sec, can be array.
609
+ :return: gst,slong,srasn,sdec. gst - greenwich mean sidereal time, slong - longitude along ecliptic
610
+ srasn - right ascension, sdec - declination of the sun (radians)
611
+ obliq - mean oblique of the ecliptic (radian)
612
+
613
+ Python version by Sheng Tian
614
+ """
615
+
616
+ twopi = 2*np.pi
617
+ jd2000 = 2451545.0
618
+ # convert to Julian date.
619
+ t0_jd = 2440587.5 # in day, 0 of Julian day.
620
+ secofdaygsw_x = 1./86400 # 1/sec of day.
621
+ t_jd = ut*secofdaygsw_x+t0_jd
622
+
623
+ # d = mjd - mj2000.
624
+ d = t_jd-jd2000
625
+ d = np.squeeze(d)
626
+
627
+ # mean obliquity of the ecliptic, e.
628
+ # e = 23.439 - 0.00000036*d ; in degree.
629
+ e = 0.4090877233749509 - 6.2831853e-9*d
630
+
631
+ # mean anomaly of the Sun, g.
632
+ # g = 357.529 + 0.98560028*d ; in degree.
633
+ g = 6.2400582213628066 + 0.0172019699945780*d
634
+ g = np.mod(g, twopi)
635
+
636
+ # mean longitude of the Sun, q.
637
+ # q = 280.459 + 0.98564736*d ; in degree.
638
+ q = 4.8949329668507771 + 0.0172027916955899*d
639
+ q = np.mod(q, twopi)
640
+
641
+ # geocentric apparent ecliptic longitude, l.
642
+ # l = q + 1.915 sin g + 0.020 sin 2g ; in degree.
643
+ l = q + 0.0334230551756914*np.sin(g) + 0.0003490658503989*np.sin(2*g)
644
+
645
+ # vl - q, mean longitude of the sun.
646
+ # vl = np.mod(279.696678+0.9856473354*dj,360.)/rad
647
+ # q = np.mod(4.881627937990388+0.01720279126623886*dj, twopi)
648
+
649
+ # g, mean anomaly of the sun.
650
+ # g = np.mod(358.475845+0.985600267*dj,360.)/rad
651
+ # g = np.mod(6.256583784118852+0.017201969767685215*dj, twopi)
652
+
653
+ # slong - l, geocentric apparent ecliptic longitude.
654
+ # slong = (vl + (1.91946-0.004789*t)*sin(g) + 0.020094*sin(2*g))/rad
655
+ # l = q+(0.03350089686033036-2.2884002156881157e-09*dj)*np.sin(g)+0.0003507064598957406*np.sin(2*g)
656
+ # l = np.mod(l, twopi)
657
+
658
+ # obliq - e, mean obliquity of the ecliptic.
659
+ # obliq = (23.45229-0.0130125*t)/rad
660
+ # e = 0.40931967763254096-6.217959450123535e-09*dj
661
+
662
+ # sin(d) = sin(e) * sin(L)
663
+ sind = np.sin(e)*np.sin(l)
664
+ sdec = np.arcsin(sind)
665
+
666
+ # tan(RA) = cos(e)*sin(L)/cos(L)
667
+ srasn = np.arctan2(np.cos(e)*np.sin(l), np.cos(l))
668
+ srasn = np.mod(srasn, twopi)
669
+
670
+
671
+ # http://aa.usno.navy.mil/faq/docs/GAST.php
672
+ # gst - gmst, greenwich mean sidereal time.
673
+ # gst = np.mod(279.690983+.9856473354*dj+360.*fday+180.,360.)/rad
674
+ # gst = np.mod(4.881528541489487+0.01720279126623886*dj+twopi*fday+np.pi, twopi)
675
+ # gmst = 18.697374558 + 24.06570982441908*d # in hour
676
+ gmst = 4.894961212735792 + 6.30038809898489*d # in rad
677
+ gmst = np.mod(gmst, twopi)
678
+
679
+ return gmst,l,srasn,sdec,e
680
+
681
+
682
+ def gswgsm(p1,p2,p3, j):
683
+ """
684
+ Converts gsm to gsw coordinates or vice versa.
685
+ j>0 j<0
686
+ input: j,xgsm,ygsm,zgsm j,xgsw,ygsw,zgsw
687
+ output: xgsw,ygsw,zgsw xgsm,ygsm,zgsm
688
+
689
+ :param p1,p2,p3: input position
690
+ :param j: flag
691
+ :return: output position
692
+ """
693
+ global e11, e21, e31, e12, e22, e32, e13, e23, e33
694
+
695
+ if j > 0:
696
+ xgsw,ygsw,zgsw = [p1,p2,p3]
697
+ xgsm = xgsw*e11 + ygsw*e12 + zgsw*e13
698
+ ygsm = xgsw*e21 + ygsw*e22 + zgsw*e23
699
+ zgsm = xgsw*e31 + ygsw*e32 + zgsw*e33
700
+ return xgsm,ygsm,zgsm
701
+ else:
702
+ xgsm,ygsm,zgsm = [p1,p2,p3]
703
+ xgsw = xgsm*e11 + ygsm*e21 + zgsm*e31
704
+ ygsw = xgsm*e12 + ygsm*e22 + zgsm*e32
705
+ zgsw = xgsm*e13 + ygsm*e23 + zgsm*e33
706
+ return xgsw,ygsw,zgsw
707
+
708
+
709
+ def geomag(p1,p2,p3, j):
710
+ """
711
+ Converts geographic (geo) to dipole (mag) coordinates or vice versa.
712
+ j>0 j<0
713
+ input: j,xgeo,ygeo,zgeo j,xmag,ymag,zmag
714
+ output: xmag,ymag,zmag xgeo,ygeo,zgeo
715
+
716
+ :param p1,p2,p3: input position
717
+ :param j: flag
718
+ :return: output position
719
+ """
720
+
721
+ # Attention: subroutine recalc must be invoked before geomag in two cases:
722
+ # /a/ before the first transformation of coordinates
723
+ # /b/ if the values of time have been changed
724
+
725
+ # common /geopack1/ st0,ct0,sl0,cl0,ctcl,stcl,ctsl,stsl,ab(19),bb(8)
726
+ global st0,ct0, sl0,sl0, ctcl,stcl, ctsl,stsl
727
+
728
+ if j > 0:
729
+ xgeo,ygeo,zgeo = [p1,p2,p3]
730
+ xmag = xgeo*ctcl+ygeo*ctsl-zgeo*st0
731
+ ymag = ygeo*cl0-xgeo*sl0
732
+ zmag = xgeo*stcl+ygeo*stsl+zgeo*ct0
733
+ return xmag,ymag,zmag
734
+ else:
735
+ xmag,ymag,zmag = [p1,p2,p3]
736
+ xgeo = xmag*ctcl-ymag*sl0+zmag*stcl
737
+ ygeo = xmag*ctsl+ymag*cl0+zmag*stsl
738
+ zgeo = zmag*ct0-xmag*st0
739
+ return xgeo,ygeo,zgeo
740
+
741
+ def geigeo(p1,p2,p3, j):
742
+ """
743
+ Converts equatorial inertial (gei) to geographical (geo) coords or vice versa.
744
+ j>0 j<0
745
+ input: j,xgei,ygei,zgei j,xgeo,ygeo,zgeo
746
+ output: xgeo,ygeo,zgeo xgei,ygei,zgei
747
+
748
+ :param p1,p2,p3: input position
749
+ :param j: flag
750
+ :return: output position
751
+ """
752
+
753
+ # Attention: subroutine recalc must be invoked before geomag in two cases:
754
+ # /a/ before the first transformation of coordinates
755
+ # /b/ if the values of time have been changed
756
+
757
+ # common /geopack1/ a(27),cgst,sgst,b(6)
758
+ global cgst,sgst
759
+
760
+ if j > 0:
761
+ xgei,ygei,zgei = [p1,p2,p3]
762
+ xgeo = xgei*cgst+ygei*sgst
763
+ ygeo = ygei*cgst-xgei*sgst
764
+ zgeo = zgei
765
+ return xgeo,ygeo,zgeo
766
+ else:
767
+ xgeo,ygeo,zgeo = [p1,p2,p3]
768
+ xgei = xgeo*cgst-ygeo*sgst
769
+ ygei = ygeo*cgst+xgeo*sgst
770
+ zgei = zgeo
771
+ return xgei,ygei,zgei
772
+
773
+ def magsm(p1,p2,p3, j):
774
+ """
775
+ Converts dipole (mag) to solar magnetic (sm) coordinates or vice versa
776
+ j>0 j<0
777
+ input: j,xmag,ymag,zmag j,xsm, ysm, zsm
778
+ output: xsm, ysm, zsm xmag,ymag,zmag
779
+
780
+ :param p1,p2,p3: input position
781
+ :param j: flag
782
+ :return: output position
783
+ """
784
+
785
+ # Attention: subroutine recalc must be invoked before geomag in two cases:
786
+ # /a/ before the first transformation of coordinates
787
+ # /b/ if the values of time have been changed
788
+
789
+ # common /geopack1/ a(8),sfi,cfi,b(7),ab(10),ba(8)
790
+ global sfi,cfi
791
+
792
+ if j > 0:
793
+ xmag,ymag,zmag = [p1,p2,p3]
794
+ xsm = xmag*cfi-ymag*sfi
795
+ ysm = xmag*sfi+ymag*cfi
796
+ zsm = zmag
797
+ return xsm,ysm,zsm
798
+ else:
799
+ xsm,ysm,zsm = [p1,p2,p3]
800
+ xmag = xsm*cfi+ysm*sfi
801
+ ymag = ysm*cfi-xsm*sfi
802
+ zmag = zsm
803
+ return xmag,ymag,zmag
804
+
805
+ def gsmgse(p1,p2,p3, j):
806
+ """
807
+ converts geocentric solar magnetospheric (gsm) coords to solar ecliptic (gse) ones or vice versa.
808
+ j>0 j<0
809
+ input: j,xgsm,ygsm,zgsm j,xgse,ygse,zgse
810
+ output: xgse,ygse,zgse xgsm,ygsm,zgsm
811
+
812
+ :param p1,p2,p3: input position
813
+ :param j: flag
814
+ :return: output position
815
+ """
816
+
817
+ # common /geopack1/ a(12),shi,chi,ab(13),ba(8)
818
+ global shi,chi
819
+
820
+ if j > 0:
821
+ xgsm,ygsm,zgsm = [p1,p2,p3]
822
+ xgse = xgsm
823
+ ygse = ygsm*chi-zgsm*shi
824
+ zgse = ygsm*shi+zgsm*chi
825
+ return xgse,ygse,zgse
826
+ else:
827
+ xgse,ygse,zgse = [p1,p2,p3]
828
+ xgsm = xgse
829
+ ygsm = ygse*chi+zgse*shi
830
+ zgsm = zgse*chi-ygse*shi
831
+ return xgsm,ygsm,zgsm
832
+
833
+ def smgsm(p1,p2,p3, j):
834
+ """
835
+ Converts solar magnetic (sm) to geocentric solar magnetospheric (gsm) coordinates or vice versa.
836
+ j>0 j<0
837
+ input: j,xsm, ysm, zsm j,xgsm,ygsm,zgsm
838
+ output: xgsm,ygsm,zgsm xsm, ysm, zsm
839
+
840
+ :param p1,p2,p3: input position
841
+ :param j: flag
842
+ :return: output position
843
+ """
844
+
845
+ # Attention: subroutine recalc must be invoked before geomag in two cases:
846
+ # /a/ before the first transformation of coordinates
847
+ # /b/ if the values of time have been changed
848
+
849
+ # common /geopack1/ a(10),sps,cps,b(15),ab(8)
850
+ global sps,cps
851
+
852
+ if j > 0:
853
+ xsm,ysm,zsm = [p1,p2,p3]
854
+ xgsm = xsm*cps+zsm*sps
855
+ ygsm = ysm
856
+ zgsm = zsm*cps-xsm*sps
857
+ return xgsm,ygsm,zgsm
858
+ else:
859
+ xgsm,ygsm,zgsm = [p1,p2,p3]
860
+ xsm = xgsm*cps-zgsm*sps
861
+ ysm = ygsm
862
+ zsm = xgsm*sps+zgsm*cps
863
+ return xsm,ysm,zsm
864
+
865
+ def geogsm(p1,p2,p3, j):
866
+ """
867
+ Converts geographic (geo) to geocentric solar magnetospheric (gsm) coordinates or vice versa.
868
+ j>0 j<0
869
+ input: j,xgeo,ygeo,zgeo j,xgsm,ygsm,zgsm
870
+ output: xgsm,ygsm,zgsm xgeo,ygeo,zgeo
871
+
872
+ :param p1,p2,p3: input position
873
+ :param j: flag
874
+ :return: output position
875
+ """
876
+
877
+ # Attention: subroutine recalc must be invoked before geomag in two cases:
878
+ # /a/ before the first transformation of coordinates
879
+ # /b/ if the values of time have been changed
880
+
881
+ # common /geopack1/aa(17),a11,a21,a31,a12,a22,a32,a13,a23,a33,d,b(8)
882
+ global a11,a21,a31,a12,a22,a32,a13,a23,a33
883
+
884
+ if j > 0:
885
+ xgeo,ygeo,zgeo = [p1,p2,p3]
886
+ xgsm = a11*xgeo+a12*ygeo+a13*zgeo
887
+ ygsm = a21*xgeo+a22*ygeo+a23*zgeo
888
+ zgsm = a31*xgeo+a32*ygeo+a33*zgeo
889
+ return xgsm,ygsm,zgsm
890
+ else:
891
+ xgsm,ygsm,zgsm = [p1,p2,p3]
892
+ xgeo = a11*xgsm+a21*ygsm+a31*zgsm
893
+ ygeo = a12*xgsm+a22*ygsm+a32*zgsm
894
+ zgeo = a13*xgsm+a23*ygsm+a33*zgsm
895
+ return xgeo,ygeo,zgeo
896
+
897
+
898
+ def geodgeo(p1,p2, j):
899
+ """
900
+ This subroutine (1) converts vertical local height (altitude) h and geodetic
901
+ latitude xmu into geocentric coordinates r and theta (geocentric radial
902
+ distance and colatitude, respectively; also known as ecef coordinates),
903
+ as well as (2) performs the inverse transformation from {r,theta} to {h,xmu}.
904
+
905
+ The subroutine uses world geodetic system wgs84 parameters for the earth's
906
+ ellipsoid. the angular quantities (geo colatitude theta and geodetic latitude
907
+ xmu) are in radians, and the distances (geocentric radius r and altitude h
908
+ above the earth's ellipsoid) are in kilometers.
909
+
910
+ if j>0, the transformation is made from geodetic to geocentric coordinates using simple direct equations.
911
+ if j<0, the inverse transformation from geocentric to geodetic coordinates is made by means of a fast iterative algorithm.
912
+
913
+ j>0 j<0
914
+ input: j, h,xmu j, r,theta
915
+ output: j, r,theta j, h,xmu
916
+
917
+ Author: N.A. Tsyganenko
918
+ Date: Dec 5, 2007
919
+
920
+ :param h: Altitude in km.
921
+ :param xmu: Geodetic latitude in radian.
922
+ :param r: Geocentric distance in km.
923
+ :param theta: Spherical co-latitude in radian.
924
+ """
925
+
926
+ # r_eq is the semi-major axis of the earth's ellipsoid,
927
+ # and beta is its second eccentricity squared
928
+ r_eq, beta = 6378.137, 6.73949674228e-3
929
+
930
+ if j>0: # Direct transformation(GEOD->GEO):
931
+ h,xmu = [p1,p2]
932
+ cosxmu = np.cos(xmu)
933
+ sinxmu = np.sin(xmu)
934
+ den = np.sqrt(cosxmu**2+(sinxmu/(1+beta))**2)
935
+ coslam = cosxmu/den
936
+ sinlam = sinxmu/(den*(1+beta))
937
+ rs = r_eq/np.sqrt(1+beta*sinlam**2)
938
+ x = rs*coslam+h*cosxmu
939
+ z = rs*sinlam+h*sinxmu
940
+ r = np.sqrt(x**2+z**2)
941
+ theta = np.arccos(z/r)
942
+ return r,theta
943
+ else: # Inverse transformation(GEO->GEOD):
944
+ r,theta = [p1,p2]
945
+ phi = np.pi*0.5-theta
946
+ phi1,dphi,h,xmu,tol = phi,0,0,0,1e-6
947
+ for n in range(100):
948
+ sp = np.sin(phi1)
949
+ arg = sp*(1+beta)/np.sqrt(1+beta*(2+beta)*sp**2)
950
+ xmu = np.arcsin(arg)
951
+ rs = r_eq/np.sqrt(1+beta*np.sin(phi1)**2)
952
+ cosfims = np.cos(phi1-xmu)
953
+ h = np.sqrt((rs*cosfims)**2+r**2-rs**2)-rs*cosfims
954
+ z = rs*np.sin(phi1)+h*np.sin(xmu)
955
+ x = rs*np.cos(phi1)+h*np.cos(xmu)
956
+ rr = np.sqrt(x**2+z**2)
957
+ dphi = np.arcsin(z/rr)-phi
958
+ phi1 -= dphi
959
+ if np.abs(dphi) <= tol: break
960
+ return h,xmu
961
+
962
+
963
+ def sphcar(p1,p2,p3, j):
964
+ """
965
+ Converts spherical coords into cartesian ones and vice versa (theta and phi in radians).
966
+ j>0 j<0
967
+ input: j,r,theta,phi j,x,y,z
968
+ output: x,y,z r,theta,phi
969
+
970
+ :param r,theta,phi:
971
+ :param x,y,z:
972
+ :param j:
973
+ :return:
974
+
975
+ Note: at the poles (x=0 and y=0) we assume phi=0 (when converting from cartesian to spherical coords, i.e., for j<0)
976
+ Last mofification: April 1, 2003 (only some notation changes and more comments added)
977
+ Author: N.A. Tsyganenko
978
+ """
979
+
980
+ if j > 0:
981
+ r,theta,phi = [p1,p2,p3]
982
+ sq=r*np.sin(theta)
983
+ x=sq*np.cos(phi)
984
+ y=sq*np.sin(phi)
985
+ z= r*np.cos(theta)
986
+ return x,y,z
987
+ else:
988
+ x,y,z = [p1,p2,p3]
989
+ sq=x**2+y**2
990
+ r=np.sqrt(sq+z**2)
991
+ if sq != 0:
992
+ sq=np.sqrt(sq)
993
+ phi=np.arctan2(y,x)
994
+ theta=np.arctan2(sq,z)
995
+ if phi < 0: phi += 2*np.pi
996
+ else:
997
+ phi=0.
998
+ if z < 0: theta = np.pi
999
+ else: theta = 0
1000
+ return r,theta,phi
1001
+
1002
+ def bspcar(theta,phi,br,btheta,bphi):
1003
+ """
1004
+ Calculates cartesian field components from spherical ones.
1005
+
1006
+ :param theta,phi: spherical angles of the point in radians
1007
+ :param br,btheta,bphi: spherical components of the field
1008
+ :return: bx,by,bz. cartesian components of the field
1009
+
1010
+ # Last mofification: April 1, 2003 (only some notation changes and more comments added)
1011
+ # Author: N.A. Tsyganenko
1012
+ """
1013
+
1014
+ s= np.sin(theta)
1015
+ c= np.cos(theta)
1016
+ sf=np.sin(phi)
1017
+ cf=np.cos(phi)
1018
+ be=br*s+btheta*c
1019
+
1020
+ bx=be*cf-bphi*sf
1021
+ by=be*sf+bphi*cf
1022
+ bz=br*c-btheta*s
1023
+
1024
+ return bx,by,bz
1025
+
1026
+ def bcarsp(x,y,z,bx,by,bz):
1027
+ """
1028
+ Calculates spherical field components from those in cartesian system
1029
+
1030
+ :param x,y,z: cartesian components of the position vector
1031
+ :param bx,by,bz: cartesian components of the field vector
1032
+ :return: br,btheta,bphi. spherical components of the field vector
1033
+
1034
+ Last mofification: April 1, 2003 (only some notation changes and more comments added)
1035
+ Author: N.A. Tsyganenko
1036
+ """
1037
+
1038
+ rho2=x**2+y**2
1039
+ r=np.sqrt(rho2+z**2)
1040
+ rho=np.sqrt(rho2)
1041
+
1042
+ if rho != 0:
1043
+ cphi=x/rho
1044
+ sphi=y/rho
1045
+ else:
1046
+ cphi=1.
1047
+ sphi=0.
1048
+
1049
+ ct=z/r
1050
+ st=rho/r
1051
+
1052
+ br=(x*bx+y*by+z*bz)/r
1053
+ btheta=(bx*cphi+by*sphi)*ct-bz*st
1054
+ bphi=by*cphi-bx*sphi
1055
+
1056
+ return br,btheta,bphi
1057
+
1058
+
1059
+ def call_external_model(exname, par, ps, x,y,z):
1060
+ if exname == 't89':
1061
+ return t89(par, ps, x, y, z)
1062
+ elif exname == 't96':
1063
+ return t96(par, ps, x, y, z)
1064
+ elif exname == 't01':
1065
+ return t01(par, ps, x, y, z)
1066
+ elif exname == 't04':
1067
+ return t04(par, ps, x, y, z)
1068
+ else:
1069
+ raise ValueError
1070
+
1071
+ def call_internal_model(inname, x,y,z):
1072
+ if inname == 'dipole':
1073
+ return dip(x,y,z)
1074
+ elif inname == 'igrf':
1075
+ return igrf_gsm(x,y,z)
1076
+ else:
1077
+ raise ValueError
1078
+
1079
+ def rhand(x,y,z,parmod,exname,inname):
1080
+ """
1081
+ Calculates the components of the right hand side vector in the geomagnetic field
1082
+ line equation (a subsidiary subroutine for the subroutine step)
1083
+
1084
+ :param x,y,z:
1085
+ :param parmod:
1086
+ :param exname: name of the subroutine for the external field.
1087
+ :param inname: name of the subroutine for the internal field.
1088
+
1089
+ Last mofification: March 31, 2003
1090
+ Author: N.A. Tsyganenko
1091
+ :return: r1,r2,r3.
1092
+ """
1093
+ # common /geopack1/ a(15),psi,aa(10),ds3,bb(8)
1094
+ global a, psi, aa, ds3, bb
1095
+
1096
+ bxgsm,bygsm,bzgsm = call_external_model(exname, parmod, psi, x,y,z)
1097
+ hxgsm,hygsm,hzgsm = call_internal_model(inname, x,y,z)
1098
+
1099
+ bx=bxgsm+hxgsm
1100
+ by=bygsm+hygsm
1101
+ bz=bzgsm+hzgsm
1102
+ b=ds3/np.sqrt(bx**2+by**2+bz**2)
1103
+
1104
+ r1=bx*b
1105
+ r2=by*b
1106
+ r3=bz*b
1107
+
1108
+ return r1,r2,r3
1109
+
1110
+ def step(x,y,z, ds,errin,parmod,exname,inname):
1111
+ """
1112
+ Re-calculates {x,y,z}, making a step along a field line.
1113
+ model version, the array parmod contains input parameters for that model
1114
+
1115
+ :param x,y,z: the input position
1116
+ :param ds: the step size
1117
+ :param errin: the permissible error value
1118
+ :param parmod: contains input parameters for that model
1119
+ :param exname: name of the subroutine for the external field.
1120
+ :param inname: name of the subroutine for the internal field.
1121
+ :return: x,y,z. The output position
1122
+
1123
+ Last mofification: March 31, 2003
1124
+ Author: N.A. Tsyganenko
1125
+ """
1126
+
1127
+ # common /geopack1/ a(26),ds3,b(8)
1128
+ global a, ds3, b
1129
+
1130
+ if errin <=0: raise ValueError
1131
+ errcur = errin
1132
+ i = 0
1133
+ maxloop = 100
1134
+
1135
+ while (errcur >= errin) & (i < maxloop):
1136
+ ds3=-ds/3.
1137
+ r11,r12,r13 = rhand(x,y,z,parmod,exname,inname)
1138
+ r21,r22,r23 = rhand(x+r11,y+r12,z+r13,parmod,exname,inname)
1139
+ r31,r32,r33 = rhand(x+.5*(r11+r21),y+.5*(r12+r22),z+.5*(r13+r23),parmod,exname,inname)
1140
+ r41,r42,r43 = rhand(x+.375*(r11+3.*r31),y+.375*(r12+3.*r32),z+.375*(r13+3.*r33),parmod,exname,inname)
1141
+ r51,r52,r53 = rhand(x+1.5*(r11-3.*r31+4.*r41),y+1.5*(r12-3.*r32+4.*r42),z+1.5*(r13-3.*r33+4.*r43),parmod,exname,inname)
1142
+ errcur=np.abs(r11-4.5*r31+4.*r41-.5*r51)+np.abs(r12-4.5*r32+4.*r42-.5*r52)+np.abs(r13-4.5*r33+4.*r43-.5*r53)
1143
+ if errcur < errin: break
1144
+
1145
+ ds *= 0.5
1146
+ i += 1
1147
+ else:
1148
+ print('reached maximum loop ...')
1149
+ return
1150
+
1151
+ x += 0.5*(r11+4.*r41+r51)
1152
+ y += 0.5*(r12+4.*r42+r52)
1153
+ z += 0.5*(r13+4.*r43+r53)
1154
+ if (errcur < (errin*0.04)) & (np.abs(ds) < 1.33):
1155
+ ds *= 1.5
1156
+
1157
+ return x,y,z
1158
+
1159
+ def trace(xi,yi,zi,dir,rlim=10,r0=1,parmod=2,exname='t89',inname='igrf',maxloop=1000):
1160
+ """
1161
+ Traces a field line from an arbitrary point of space to the earth's surface or
1162
+ to a model limiting boundary.
1163
+
1164
+ The highest order of spherical harmonics in the main field expansion used
1165
+ in the mapping is calculated automatically. if inname=igrf_gsm, then an IGRF model
1166
+ field will be used, and if inname=dip, a pure dipole field will be used.
1167
+
1168
+ In any case, before calling trace, one should invoke recalc, to calculate correct
1169
+ values of the IGRF coefficients and all quantities needed for transformations
1170
+ between coordinate systems involved in this calculations.
1171
+
1172
+ Alternatively, the subroutine recalc can be invoked with the desired values of
1173
+ ut sec (to specify the dipole moment), while the values of the dipole
1174
+ tilt angle psi (in radians) and its sine (sps) and cosine (cps) can be explicitly
1175
+ specified and forwarded to the common block geopack1 (11th, 12th, and 16th elements, resp.)
1176
+
1177
+ :param xi,yi,zi: gsm coords of initial point (in earth radii, 1 re = 6371.2 km). Must be outside the sphere of r0,
1178
+ otherwise tracing will not start.
1179
+ :param dir: sign of the tracing direction: if
1180
+ dir=1.0 then we move antiparallel to the field vector (e.g. from northern to southern conjugate point), and if
1181
+ dir=-1.0 then the tracing goes in the opposite direction.
1182
+ :param rlim: upper limit of the geocentric distance, where the tracing is terminated.
1183
+ :param r0: radius of a sphere (in re) for which the field line endpoint coordinates xf,yf,zf should be calculated.
1184
+ :param parmod: the iopt for T89, or for the other Txx models it is
1185
+ a 10-element array containing model parameters, needed for a unique specification of the external field.
1186
+ The concrete meaning of the components of parmod depends on a specific version of the external field model.
1187
+ :param exname: name of the subroutine for the external field.
1188
+ :param inname: name of the subroutine for the internal field.
1189
+ :return:
1190
+ xf,yf,zf. GSM coords of the last calculated point of a field line
1191
+ xx,yy,zz. Arrays containing coords of field line points. Here their maximal length was assumed equal to 999.
1192
+ l. Actual number of the calculated field line points. if l exceeds 999, tracing terminates, and a warning is displayed.
1193
+
1194
+ Last mofification: March 31, 2003
1195
+ Author: N.A. Tsyganenko
1196
+ """
1197
+
1198
+ # common /geopack1/ aa(26),dd,bb(8)
1199
+ global aa, dd, bb, ds3
1200
+
1201
+ err, l, ds, x,y,z, dd, al = 0.001, 0, 0.5*dir, xi,yi,zi, dir, 0.
1202
+ xx = np.array([x])
1203
+ yy = np.array([y])
1204
+ zz = np.array([z])
1205
+
1206
+ # Here we call RHAND just to find out the sign of the radial component of the field
1207
+ # vector, and to determine the initial direction of the tracing (i.e., either away
1208
+ # or towards Earth):
1209
+ ds3 = -ds/3.
1210
+ r1,r2,r3 = rhand(x,y,z,parmod,exname,inname)
1211
+
1212
+ # |ad|=0.01 and its sign follows the rule:
1213
+ # (1) if dir=1 (tracing antiparallel to B vector) then the sign of ad is the same as of Br
1214
+ # (2) if dir=-1 (tracing parallel to B vector) then the sign of ad is opposite to that of Br
1215
+ # ad is defined in order to initialize the value of rr (radial distance at previous step):
1216
+ ad=0.01
1217
+ if (x*r1+y*r2+z*r3) < 0: ad=-0.01
1218
+
1219
+ rr=np.sqrt(x**2+y**2+z**2)+ad
1220
+
1221
+ while l < maxloop:
1222
+ ryz=y**2+z**2
1223
+ r2=x**2+ryz
1224
+ r=np.sqrt(r2)
1225
+ xr,yr,zr = [x,y,z]
1226
+
1227
+ # check if the line hit the outer tracing boundary; if yes, then terminate the tracing
1228
+ if (r >= rlim) | (ryz >= 1600) | (x>=20): break
1229
+
1230
+ # check whether or not the inner tracing boundary was crossed from outside,
1231
+ # if yes, then calculate the footpoint position by interpolation
1232
+ if (r < r0) & (rr > r): # this is crossing the inner boundary from inside!!!??? looks like it will trace into the boundary and then bounce back out.
1233
+ # find the footpoint position by interpolating between the current and previous field line points:
1234
+ r1=(r0-r)/(rr-r)
1235
+ x=x-(x-xr)*r1
1236
+ y=y-(y-yr)*r1
1237
+ z=z-(z-zr)*r1
1238
+ xr,yr,zr = [x,y,z]
1239
+ break
1240
+
1241
+ # check if (i) we are moving outward, or (ii) we are still sufficiently
1242
+ # far from Earth (beyond R=5Re); if yes, proceed further:
1243
+ if (r >= rr) | (r > 5):
1244
+ pass
1245
+ # now we moved closer inward (between R=3 and R=5); go to 3 and begin logging
1246
+ # previous values of X,Y,Z, to be used in the interpolation (after having
1247
+ # crossed the inner tracing boundary):
1248
+ else:
1249
+ if r >= 3:
1250
+ # We entered inside the sphere R=3: to avoid too large steps (and hence inaccurate
1251
+ # interpolated position of the footpoint), enforce the progressively smaller
1252
+ # stepsize values as we approach the inner boundary R=R0:
1253
+ ds = dir
1254
+ else:
1255
+ fc = 0.2
1256
+ if (r-r0) < 0.05: fc = 0.05
1257
+ al = fc*(r-r0+0.2)
1258
+ ds = dir*al
1259
+ rr=r
1260
+ x,y,z = step(x,y,z,ds,err,parmod,exname,inname)
1261
+ xx = np.append(xx,x)
1262
+ yy = np.append(yy,y)
1263
+ zz = np.append(zz,z)
1264
+ l += 1
1265
+
1266
+ return x,y,z, xx,yy,zz
1267
+
1268
+ def shuetal_mgnp(xn_pd,vel,bzimf,xgsm,ygsm,zgsm):
1269
+ """
1270
+ For any point of space with coordinates (xgsm,ygsm,zgsm) and specified conditions
1271
+ in the incoming solar wind, this subroutine:
1272
+ (1) determines if the point (xgsm,ygsm,zgsm) lies inside or outside the
1273
+ model magnetopause of Shue et al. (jgr-a, v.103, p. 17691, 1998).
1274
+ (2) calculates the gsm position of a point {xmgnp,ymgnp,zmgnp}, lying at the model
1275
+ magnetopause and asymptotically tending to the nearest boundary point with
1276
+ respect to the observation point {xgsm,ygsm,zgsm}, as it approaches the magnetopause.
1277
+
1278
+ :param xn_pd: either solar wind proton number density (per c.c.) (if vel>0)
1279
+ or the solar wind ram pressure in nanopascals (if vel<0)
1280
+ :param vel: either solar wind velocity (km/sec)
1281
+ or any negative number, which indicates that xn_pd stands
1282
+ for the solar wind pressure, rather than for the density
1283
+ :param bzimf: imf bz in nanoteslas
1284
+ :param xgsm,ygsm,zgsm: gsm position of the observation point in earth radii
1285
+ :return: xmgnp,ymgnp,zmgnp. GSM position of the boundary point.
1286
+ dist. Distance (in re) between the observation point (xgsm,ygsm,zgsm) and the model magnetopause
1287
+ id. position flag: id=+1 (-1) means that the observation point lies inside (outside) of the model magnetopause, respectively.
1288
+
1289
+ Last mofification: April 4, 2003
1290
+ Author: N.A. Tsyganenko
1291
+ """
1292
+
1293
+ # pd is the solar wind dynamic pressure (in npa)
1294
+ if vel < 0: pd = xn_pd
1295
+ else: pd = 1.94e-6*xn_pd*vel**2
1296
+
1297
+ # Define the angle phi, measured duskward from the noon-midnight meridian plane;
1298
+ # if the observation point lies on the x axis, the angle phi cannot be uniquely
1299
+ # defined, and we set it at zero:
1300
+ if (ygsm != 0) | (zgsm != 0): phi = np.arctan2(ygsm,zgsm)
1301
+ else: phi = 0
1302
+
1303
+ # First, find out if the observation point lies inside the Shue et al bdry
1304
+ # and set the value of the id flag:
1305
+ id = -1
1306
+ r0 = (10.22+1.29*np.tanh(0.184*(bzimf+8.14)))*pd**(-0.15151515)
1307
+ alpha = (0.58-0.007*bzimf)*(1.+0.024*np.log(pd))
1308
+ r = np.sqrt(xgsm**2+ygsm**2+zgsm**2)
1309
+ rm = r0*(2./(1.+xgsm/r))**alpha
1310
+ if r < rm: id = 1
1311
+
1312
+ # Now, find the corresponding t96 magnetopause position, to be used as
1313
+ # a starting approximation in the search of a corresponding Shue et al.
1314
+ # boundary point:
1315
+ xmt96,ymt96,zmt96,dist,id96 = t96_mgnp(pd,-1.,xgsm,ygsm,zgsm)
1316
+
1317
+ rho2 = ymt96**2+zmt96**2
1318
+ r = np.sqrt(rho2+xmt96**2)
1319
+ st = np.sqrt(rho2)/r
1320
+ ct = xmt96/r
1321
+
1322
+ # Now, use newton's iterative method to find the nearest point at the Shue et al.'s boundary:
1323
+ nit = 0
1324
+ while nit < 1000:
1325
+ nit += 1
1326
+
1327
+ t = np.arctan2(st,ct)
1328
+ rm = r0*(2./(1.+ct))**alpha
1329
+
1330
+ f = r-rm
1331
+ gradf_r = 1.
1332
+ gradf_t = -alpha/r*rm*st/(1.+ct)
1333
+ gradf = np.sqrt(gradf_r**2+gradf_t**2)
1334
+
1335
+ dr = -f/gradf**2
1336
+ dt = dr/r*gradf_t
1337
+
1338
+ r = r+dr
1339
+ t = t+dt
1340
+ st = np.sin(t)
1341
+ ct = np.cos(t)
1342
+
1343
+ ds = np.sqrt(dr**2+(r*dt)**2)
1344
+ if ds <= 1.e-4: break
1345
+ else: print(' boundary point could not be found; iterations do not converge')
1346
+
1347
+
1348
+ xmgnp = r*np.cos(t)
1349
+ rho = r*np.sin(t)
1350
+ ymgnp = rho*np.sin(phi)
1351
+ zmgnp = rho*np.cos(phi)
1352
+
1353
+ dist = np.sqrt((xgsm-xmgnp)**2+(ygsm-ymgnp)**2+(zgsm-zmgnp)**2)
1354
+
1355
+ return xmgnp,ymgnp,zmgnp, dist, id
1356
+
1357
+ def t96_mgnp(xn_pd,vel,xgsm,ygsm,zgsm):
1358
+ """
1359
+ For any point of space with given coordinates (xgsm,ygsm,zgsm), this subroutine defines
1360
+ the position of a point (xmgnp,ymgnp,zmgnp) at the T96 model magnetopause, having the
1361
+ same value of the ellipsoidal tau-coordinate, and the distance between them. This is
1362
+ not the shortest distance d_min to the boundary, but dist asymptotically tends to d_min,
1363
+ as the observation point gets closer to the magnetopause.
1364
+
1365
+ The pressure-dependent magnetopause is that used in the t96_01 model
1366
+ (Tsyganenko, jgr, v.100, p.5599, 1995; esa sp-389, p.181, oct. 1996)
1367
+
1368
+ :param xn_pd: either solar wind proton number density (per c.c.) (if vel>0)
1369
+ or the solar wind ram pressure in nanopascals (if vel<0)
1370
+ :param vel: either solar wind velocity (km/sec)
1371
+ or any negative number, which indicates that xn_pd stands
1372
+ for the solar wind pressure, rather than for the density
1373
+ :param xgsm,ygsm,zgsm: gsm position of the observation point in earth radii
1374
+ :return: xmgnp,ymgnp,zmgnp. GSM position of the boundary point.
1375
+ dist. Distance (in re) between the observation point (xgsm,ygsm,zgsm) and the model magnetopause
1376
+ id. position flag: id=+1 (-1) means that the observation point lies inside (outside) of the model magnetopause, respectively.
1377
+
1378
+ Last mofification: April 3, 2003
1379
+ Author: N.A. Tsyganenko
1380
+ """
1381
+ # Define solar wind dynamic pressure (nanopascals, assuming 4% of alpha-particles),
1382
+ # if not explicitly specified in the input:
1383
+ if vel < 0: pd = xn_pd
1384
+ else: pd = 1.94e-6*xn_pd*vel**2
1385
+
1386
+ # ratio of pd to the average pressure, assumed equal to 2 npa:
1387
+ # The power index 0.14 in the scaling factor is the best-fit value
1388
+ # obtained from data and used in the t96_01 version
1389
+ # Values of the magnetopause parameters for pd = 2 npa:
1390
+ rat = pd/2.0
1391
+ rat16 = rat**0.14
1392
+
1393
+ a0, s00, x00 = [70.,1.08,5.48]
1394
+
1395
+ # Values of the magnetopause parameters, scaled by the actual pressure:
1396
+ # xm is the x-coordinate of the "seam" between the ellipsoid and the cylinder
1397
+ # For details on the ellipsoidal coordinates, see the paper:
1398
+ # N.A. Tsyganenko, Solution of chapman-ferraro problem for an ellipsoidal magnetopause, planet.space sci., v.37, p.1037, 1989).
1399
+ a = a0/rat16
1400
+ s0 = s00
1401
+ x0 = x00/rat16
1402
+ xm = x0-a
1403
+
1404
+
1405
+ if (ygsm != 0) | (zgsm != 0): phi = np.arctan2(ygsm,zgsm)
1406
+ else: phi = 0
1407
+
1408
+ rho = np.sqrt(ygsm**2+zgsm**2)
1409
+ if xgsm < xm:
1410
+ xmgnp = xgsm
1411
+ rhomgnp = a*np.sqrt(s0**2-1)
1412
+ ymgnp = rhomgnp*np.sin(phi)
1413
+ zmgnp = rhomgnp*np.cos(phi)
1414
+ dist = np.sqrt((xgsm-xmgnp)**2+(ygsm-ymgnp)**2+(zgsm-zmgnp)**2)
1415
+ if rhomgnp > rho: id = 1
1416
+ else: id = -1
1417
+ return xmgnp,ymgnp,zmgnp, dist, id
1418
+
1419
+ xksi = (xgsm-x0)/a+1.
1420
+ xdzt = rho/a
1421
+ sq1 = np.sqrt((1.+xksi)**2+xdzt**2)
1422
+ sq2 = np.sqrt((1.-xksi)**2+xdzt**2)
1423
+ sigma = 0.5*(sq1+sq2)
1424
+ tau = 0.5*(sq1-sq2)
1425
+
1426
+ # Now calculate (x,y,z) for the closest point at the magnetopause
1427
+ xmgnp = x0-a*(1.-s0*tau)
1428
+ arg = (s0**2-1.)*(1.-tau**2)
1429
+ if arg < 0: arg = 0
1430
+ rhomgnp = a*np.sqrt(arg)
1431
+ ymgnp = rhomgnp*np.sin(phi)
1432
+ zmgnp = rhomgnp*np.cos(phi)
1433
+
1434
+ # Now calculate the distance between the points {xgsm,ygsm,zgsm} and {xmgnp,ymgnp,zmgnp}:
1435
+ # (in general, this is not the shortest distance d_min, but dist asymptotically tends
1436
+ # to d_min, as we are getting closer to the magnetopause):
1437
+ dist = np.sqrt((xgsm-xmgnp)**2+(ygsm-ymgnp)**2+(zgsm-zmgnp)**2)
1438
+ if sigma > s0: id = -1 # id = -1 means that the point lies outside
1439
+ else: id = 1 # id = 1 means that the point lies inside
1440
+
1441
+ return xmgnp,ymgnp,zmgnp, dist, id
1442
+
1443
+
1444
+ init_igrf()
1445
+
1446
+
1447
+ if __name__ == '__main__':
1448
+ update_igrf()
1449
+ init_igrf()