tabsim 0.0.1__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.
@@ -0,0 +1,1082 @@
1
+ import jax.numpy as jnp
2
+ from jax import jit, vmap, jacrev, config, Array
3
+ from tabsim.jax.utils import jit_with_doc
4
+
5
+ config.update("jax_enable_x64", True)
6
+
7
+ # Constants
8
+ G = 6.67408e-11 # Gravitational constant in m^3/kg/s^2
9
+ M_e = 5.9722e24 # Mass of the Earth in kilograms
10
+ R_e = 6.371e6 # Average radius of the Earth in metres
11
+ T_s = 86164.0905 # Sidereal day in seconds
12
+ Omega_e = 2 * jnp.pi / T_s # Earth rotation rate in rad/s
13
+ C = 299792458.0 # Speed of light in m/s
14
+ DAY_SECS = 24 * 3600.0 # Seconds in a day
15
+
16
+
17
+ def secs_to_days(seconds):
18
+
19
+ return seconds / DAY_SECS
20
+
21
+
22
+ def days_to_secs(days):
23
+
24
+ return days * DAY_SECS
25
+
26
+
27
+ @jit_with_doc
28
+ def radec_to_lmn(ra: Array, dec: Array, phase_centre: Array) -> Array:
29
+ """
30
+ Convert right-ascension and declination positions of a set of sources to
31
+ direction cosines.
32
+
33
+ Parameters
34
+ ----------
35
+ ra : ndarray (n_src,)
36
+ Right-ascension in degrees.
37
+ dec : ndarray (n_src,)
38
+ Declination in degrees.
39
+ phase_centre : ndarray (2,)
40
+ The ra and dec coordinates of the phase centre in degrees.
41
+
42
+ Returns
43
+ -------
44
+ lmn : ndarray (n_src, 3)
45
+ The direction cosines, (l,m,n), coordinates of each source.
46
+ """
47
+ ra = jnp.asarray(ra)
48
+ dec = jnp.asarray(dec)
49
+ phase_centre = jnp.asarray(phase_centre)
50
+ ra, dec = jnp.deg2rad(jnp.array([ra, dec]))
51
+ phase_centre = jnp.deg2rad(phase_centre)
52
+
53
+ delta_ra = ra - phase_centre[0]
54
+ dec_0 = phase_centre[1]
55
+
56
+ l = jnp.cos(dec) * jnp.sin(delta_ra)
57
+ m = jnp.sin(dec) * jnp.cos(dec_0) - jnp.cos(dec) * jnp.sin(dec_0) * jnp.cos(
58
+ delta_ra
59
+ )
60
+ n = jnp.sqrt(1 - l**2 - m**2)
61
+
62
+ return jnp.array([l, m, n]).T
63
+
64
+
65
+ @jit_with_doc
66
+ def radec_to_XYZ(ra: Array, dec: Array) -> Array:
67
+ """
68
+ Convert Right ascension and Declination to unit vector in ECI coordinates.
69
+
70
+ Parameters
71
+ ----------
72
+ ra : ndarray (n_src,)
73
+ Right-ascension in degrees.
74
+ dec : ndarray (n_src,)
75
+ Declination in degrees.
76
+
77
+ Returns
78
+ -------
79
+ xyz: ndarray (n_src, 3) or (3,)
80
+ The ECI coordinate unit vector of each source.
81
+ """
82
+ ra = jnp.asarray(ra)
83
+ dec = jnp.asarray(dec)
84
+ ra, dec = jnp.deg2rad(jnp.array([ra, dec]))
85
+ x = jnp.cos(ra) * jnp.cos(dec)
86
+ y = jnp.sin(ra) * jnp.cos(dec)
87
+ z = jnp.sin(dec)
88
+
89
+ return jnp.array([x, y, z]).T
90
+
91
+
92
+ # @jit_with_doc
93
+ # def radec_to_altaz(ra: float, dec: float, latitude: float, longitude: float, times: Array) -> Array:
94
+ # """
95
+ # !! Do not use this function - Needs to be checked !!
96
+ # Convert Right ascension and Declination to unit vector in ECI coordinates.
97
+
98
+ # Parameters
99
+ # ----------
100
+ # ra : float
101
+ # Right-ascension in degrees.
102
+ # dec : float
103
+ # Declination in degrees.
104
+ # latitude: float
105
+ # The latitude of the observer in degrees.
106
+ # times: ndarray (n_time,)
107
+ # The time of each position in seconds.
108
+
109
+ # Returns
110
+ # -------
111
+ # altaz: ndarray (n_time, 2)
112
+ # The altiude and azimuth of the source at each time.
113
+ # """
114
+ # ra, dec = jnp.deg2rad(jnp.asarray([ra, dec]))
115
+ # lat, lon = jnp.deg2rad(jnp.asarray([latitude, longitude]))
116
+ # times = jnp.asarray(times)
117
+ # gmst = 2.0 * jnp.pi * times / T_s
118
+ # ha = (gmst + lon - ra) % 2 * jnp.pi
119
+
120
+ # alt = jnp.arcsin(
121
+ # jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(ha)
122
+ # )
123
+
124
+ # az = jnp.arctan2(
125
+ # -jnp.sin(ha), jnp.cos(ha) * jnp.sin(lat) - jnp.tan(dec) * jnp.cos(lat)
126
+ # )
127
+
128
+ # # az = jnp.arccos(
129
+ # # (jnp.sin(dec) - jnp.sin(alt) * jnp.sin(lat)) / (jnp.cos(alt) * jnp.cos(lat))
130
+ # # )
131
+ # # az = jnp.where(ha < 0, 2 * jnp.pi - az, az)
132
+
133
+ # # az = jnp.arcsin(
134
+ # # jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(ha)
135
+ # # )
136
+ # # az = jnp.where(ha < jnp.pi, 2 * jnp.pi - az, az)
137
+
138
+ # return jnp.rad2deg(jnp.array([alt, az]).T)
139
+
140
+
141
+ @jit_with_doc
142
+ def ENU_to_GEO(geo_ref: Array, enu: Array) -> Array:
143
+ """
144
+ Convert a set of points in ENU co-ordinates to geographic coordinates i.e.
145
+ (latitude, longitude, elevation).
146
+
147
+ Parameters
148
+ ----------
149
+ geo_ref: ndarray (3,)
150
+ The latitude, longitude and elevation, (lat,lon,elevation), of the
151
+ reference position i.e. ENU = (0,0,0).
152
+ enu: ndarray (n_ants, 3)
153
+ The ENU coordinates of each antenna. (East, North, Up).
154
+
155
+ Returns
156
+ -------
157
+ geo_ants: ndarray (n_ant, 3)
158
+ The geographic coordinates, (lat,lon,elevation), of each antenna.
159
+ """
160
+ geo_ref = jnp.asarray(geo_ref)
161
+ enu = jnp.asarray(enu)
162
+ R_e = earth_radius(geo_ref[0])
163
+ d_lon = jnp.rad2deg(
164
+ jnp.arcsin(enu[:, 1] / (R_e * jnp.cos(jnp.deg2rad(geo_ref[0]))))
165
+ )
166
+ d_lat = jnp.rad2deg(jnp.arcsin(enu[:, 0] / R_e))
167
+ geo_ants = jnp.array(geo_ref)[None, :] + jnp.array([d_lat, d_lon, enu[:, -1]]).T
168
+
169
+ return geo_ants
170
+
171
+
172
+ @jit_with_doc
173
+ def GEO_to_XYZ(geo: Array, times: Array) -> Array:
174
+ """
175
+ Convert geographic coordinates to an Earth Centred Inertial (ECI)
176
+ coordinate frame. This is different to ECEF as ECI remains fixed with the
177
+ celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
178
+ sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
179
+ and +x is in the plane of the Equator passing through the Meridian at t = 0
180
+ and +y is also in the plane of the Equator and passes through 90 degrees
181
+ East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
182
+
183
+ Parameters
184
+ ----------
185
+ geo: ndarray (n_time, 3)
186
+ The geographic coordinates, (lat,lon,elevation), at each point in time.
187
+ times: ndarray (n_time,)
188
+ The time of each position in seconds.
189
+
190
+ Returns
191
+ -------
192
+ xyz: ndarray (n_time, 3)
193
+ The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
194
+ """
195
+ times = jnp.asarray(times)
196
+ geo = jnp.asarray(geo) * jnp.ones((len(times), 3))
197
+ lat, lon, elevation = geo.T
198
+ R_e = earth_radius(lat)
199
+ r = R_e + elevation
200
+ lat = jnp.deg2rad(lat)
201
+ lon = jnp.deg2rad(lon)
202
+ omega = 2.0 * jnp.pi / T_s
203
+
204
+ x = r * jnp.cos(lon + (omega * times)) * jnp.cos(lat)
205
+ y = r * jnp.sin(lon + (omega * times)) * jnp.cos(lat)
206
+ z = r * jnp.sin(lat)
207
+
208
+ return jnp.array([x, y, z]).T
209
+
210
+
211
+ @jit_with_doc
212
+ def GEO_to_XYZ_vmap0(geo: Array, times: Array) -> Array:
213
+ """
214
+ Convert geographic coordinates to an Earth Centred Inertial (ECI)
215
+ coordinate frame. This is different to ECEF as ECI remains fixed with the
216
+ celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
217
+ sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
218
+ and +x is in the plane of the Equator passing through the Meridian at t = 0
219
+ and +y is also in the plane of the Equator and passes through 90 degrees
220
+ East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
221
+
222
+ Parameters
223
+ ----------
224
+ geo: ndarray (n_src, n_time, 3)
225
+ The geographic coordinates, (lat,lon,elevation), at each point in time.
226
+ times: ndarray (n_time,)
227
+ The time of each position in seconds.
228
+
229
+ Returns
230
+ -------
231
+ xyz: ndarray (n_src, n_time, 3)
232
+ The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
233
+ """
234
+ return vmap(GEO_to_XYZ, in_axes=(0, None), out_axes=0)(geo, times)
235
+
236
+
237
+ @jit_with_doc
238
+ def GEO_to_XYZ_vmap1(geo: Array, times: Array) -> Array:
239
+ """
240
+ Convert geographic coordinates to an Earth Centred Inertial (ECI)
241
+ coordinate frame. This is different to ECEF as ECI remains fixed with the
242
+ celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
243
+ sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
244
+ and +x is in the plane of the Equator passing through the Meridian at t = 0
245
+ and +y is also in the plane of the Equator and passes through 90 degrees
246
+ East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
247
+
248
+ Parameters
249
+ ----------
250
+ geo: ndarray (n_time, n_ant, 3)
251
+ The geographic coordinates, (lat,lon,elevation), at each point in time.
252
+ times: ndarray (n_time,)
253
+ The time of each position in seconds.
254
+
255
+ Returns
256
+ -------
257
+ xyz: ndarray (n_time, n_ant, 3)
258
+ The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
259
+ """
260
+ return vmap(GEO_to_XYZ, in_axes=(1, None), out_axes=1)(geo, times)
261
+
262
+
263
+ @jit_with_doc
264
+ def alt_az_of_source(lsa: Array, lat: float, ra: float, dec: float) -> Array:
265
+ """Calculate the altitude and azimuth of a given source direction over time.
266
+ Taken from https://astronomy.stackexchange.com/questions/14492/need-simple-equation-for-rise-transit-and-set-time
267
+
268
+ Parameters
269
+ ----------
270
+ lst : Array (n_time,)
271
+ Local sidereal angle in degrees.
272
+ lat : float
273
+ Latitude of the observer in degrees.
274
+ ra : float
275
+ Right ascension of the source in degrees.
276
+ dec : float
277
+ DEclination of the source in degrees.
278
+
279
+ Returns
280
+ -------
281
+ Array (n_time, 2)
282
+ Altitude and azimuth of the source in degrees relative to the observer.
283
+ """
284
+
285
+ h0 = jnp.atleast_1d(lsa) - ra
286
+ ones = jnp.ones_like(h0)
287
+ lat, h0, dec = jnp.deg2rad(jnp.array([lat * ones, h0, dec * ones]))
288
+
289
+ a = jnp.cos(lat) * jnp.sin(dec) - jnp.cos(dec) * jnp.sin(lat) * jnp.cos(h0)
290
+ b = jnp.cos(dec) * jnp.sin(h0)
291
+ c = jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(h0)
292
+
293
+ alt = jnp.rad2deg(jnp.arctan2(c, jnp.sqrt(a**2 + b**2)))
294
+ az = jnp.rad2deg(jnp.arctan2(b, a))
295
+
296
+ return jnp.array([alt, az]).T
297
+
298
+
299
+ @jit_with_doc
300
+ def rise_and_set_of_source(lat: float, ra: float, dec: float) -> Array:
301
+
302
+ lat, dec = jnp.deg2rad(jnp.array([lat, dec]))
303
+
304
+ a = jnp.rad2deg(jnp.arccos(-jnp.tan(dec) * jnp.tan(lat)))
305
+
306
+ return jnp.array([ra - a, ra + a])
307
+
308
+
309
+ @jit_with_doc
310
+ def lst_deg2sec(lst: Array) -> Array:
311
+ """Convert a sidereal time in degrees to seconds.
312
+
313
+ Parameters
314
+ ----------
315
+ lst : Array
316
+ Sidereal time in degrees to convert.
317
+
318
+ Returns
319
+ -------
320
+ Array
321
+ Sidereal time in seconds.
322
+ """
323
+
324
+ return lst / 360 * T_s
325
+
326
+
327
+ @jit_with_doc
328
+ def lst_sec2deg(lst: Array) -> Array:
329
+ """Convert a sidereal time from seconds to degrees.
330
+
331
+ Parameters
332
+ ----------
333
+ lst : Array
334
+ Sidereal time in seconds to convert.
335
+
336
+ Returns
337
+ -------
338
+ Array
339
+ Sidereal time in degrees.
340
+ """
341
+
342
+ return lst / T_s * 360
343
+
344
+
345
+ def gmsa_from_jd(jd: float) -> float:
346
+ """Get the Greenwich Mean Sidereal Angle in degrees from the Julian Day (UT1).
347
+ Calculated using https://aa.usno.navy.mil/faq/GAST
348
+
349
+ Parameters
350
+ ----------
351
+ jd : float
352
+ Julian Day (UT1).
353
+
354
+ Returns
355
+ -------
356
+ float
357
+ Greenwich Mean Sidereal Angle in degrees.
358
+ """
359
+
360
+ gmst_hours = 18.697375 + 24.065709824279 * (jd - 2451545.0)
361
+
362
+ gmsa = gmst_hours * 15
363
+
364
+ return gmsa
365
+
366
+
367
+ def jd_from_gmst(gmst: float) -> float:
368
+ """Get the Julian Date (UT1) from the Greenwich Mean Sidereal Time in seconds.
369
+
370
+ Parameters
371
+ ----------
372
+ gmst : float
373
+ Greenwich Mean Sidereal Time in seconds.
374
+
375
+ Returns
376
+ -------
377
+ float
378
+ Julian Date (UT1).
379
+ """
380
+
381
+ gmst_hours = gmst / 3600
382
+
383
+ jd = (gmst_hours - 18.697375) / 24.065709824279 + 2451545.0
384
+
385
+ return jd
386
+
387
+
388
+ def jd_to_mjd(jd):
389
+
390
+ mjd = jd - 2400000.5
391
+
392
+ return mjd
393
+
394
+
395
+ def mjd_to_jd(mjd):
396
+
397
+ jd = mjd + 2400000.5
398
+
399
+ return jd
400
+
401
+
402
+ def time_above_horizon(lat: float, dec: float) -> float:
403
+ """
404
+ The number of degrees an object is above the horizon in a given day.
405
+ """
406
+
407
+ lat, dec = jnp.deg2rad(jnp.array([lat, dec]))
408
+
409
+ if jnp.cos(dec) == 0 or jnp.cos(lat) == 0:
410
+ return jnp.inf
411
+
412
+ H = 2 * jnp.rad2deg(jnp.arccos(-jnp.tan(lat) * jnp.tan(dec)))
413
+
414
+ return H
415
+
416
+
417
+ @jit_with_doc
418
+ def transit_altitude(lat: float, dec: float) -> float:
419
+ """Calculate the altitude of a source at transit given an observer's latitude.
420
+
421
+ Parameters
422
+ ----------
423
+ lat : float
424
+ Latitude of the observer in degrees.
425
+ dec : float
426
+ Declination of the source in degrees.
427
+
428
+ Returns
429
+ -------
430
+ float
431
+ Altitude of the source at transit in degrees.
432
+ """
433
+
434
+ alt = 90 - jnp.abs(dec - lat)
435
+
436
+ return alt
437
+
438
+
439
+ @jit_with_doc
440
+ def earth_radius(lat: float) -> float:
441
+ """Calculate the earth radius according to the ellipsoidal model at a given latitude.
442
+
443
+ Parameters
444
+ ----------
445
+ lat : float
446
+ Latitude of the location in degrees.
447
+
448
+ Returns
449
+ -------
450
+ float
451
+ Earth radius in metres at the given latitude.
452
+ """
453
+ a = 6378137.0 # equitorial radius
454
+ b = 6356752.3 # polar radius
455
+ lat = jnp.deg2rad(lat)
456
+ cos = jnp.cos(lat)
457
+ sin = jnp.sin(lat)
458
+ r = jnp.sqrt(
459
+ ((a**2 * cos) ** 2 + (b**2 * sin) ** 2) / ((a * cos) ** 2 + (b * sin) ** 2)
460
+ )
461
+
462
+ return r
463
+
464
+
465
+ @jit_with_doc
466
+ def enu_to_itrf(enu: Array, lat: float, lon: float, el: float) -> Array:
467
+ """
468
+ Calculate ITRF coordinates from ENU coordinates of antennas given the
469
+ latitude and longitude of the antenna array centre.
470
+
471
+ Paramters
472
+ ---------
473
+ enu: Array (n_ant, 3)
474
+ The East, North, Up coordinates of each antenna.
475
+ lat: float
476
+ The latitude of the observer/telescope.
477
+ lon: float
478
+ The longitude of the observer/telescope.
479
+ el: float
480
+ The elevation of the observer/telescope.
481
+
482
+ Returns
483
+ -------
484
+ itrf: Array (n_ant, 3)
485
+ The ITRF coordinates of the antennas.
486
+ """
487
+
488
+ enu = jnp.atleast_2d(enu)
489
+ R = earth_radius(lat) + el
490
+ lat, lon = jnp.deg2rad(jnp.array([lat, lon]))
491
+
492
+ r0 = R * jnp.array(
493
+ [jnp.cos(lat) * jnp.cos(lon), jnp.cos(lat) * jnp.sin(lon), jnp.sin(lat)]
494
+ )
495
+
496
+ R = jnp.array(
497
+ [
498
+ [-jnp.sin(lon), jnp.cos(lon), 0],
499
+ [-jnp.cos(lon) * jnp.sin(lat), -jnp.sin(lon) * jnp.sin(lat), jnp.cos(lat)],
500
+ [jnp.cos(lat) * jnp.cos(lon), jnp.cos(lat) * jnp.sin(lon), jnp.sin(lat)],
501
+ ]
502
+ )
503
+
504
+ return r0[None, :] + jnp.dot(enu, R)
505
+
506
+
507
+ def enu_to_xyz_local(enu, lat):
508
+ """
509
+ https://web.njit.edu/~gary/728/Lecture6.html
510
+ """
511
+ enu = jnp.atleast_2d(enu)
512
+ lat = jnp.deg2rad(lat)
513
+
514
+ R = jnp.array(
515
+ [
516
+ [0, -jnp.sin(lat), jnp.cos(lat)],
517
+ [1, 0, 0],
518
+ [0, jnp.cos(lat), jnp.sin(lat)],
519
+ ]
520
+ )
521
+
522
+ return jnp.dot(enu, R.T)
523
+
524
+
525
+ @jit_with_doc
526
+ def itrf_to_geo(itrf: Array) -> Array:
527
+ """Convert ITRF coordinates to geodetic coordinates.
528
+
529
+ Parameters
530
+ ----------
531
+ itrf : Array (n_ant, 3)
532
+ ITRF coordinates.
533
+
534
+ Returns
535
+ -------
536
+ Array (n_ant, 3)
537
+ Geodetic coordinates (latitude, longitude, elevation)
538
+ """
539
+ x, y, z = jnp.atleast_2d(itrf).T
540
+ xy = jnp.sqrt(x**2 + y**2)
541
+
542
+ lat = jnp.rad2deg(jnp.arcsin(z / xy))
543
+ lon = jnp.rad2deg(jnp.arctan2(y, x))
544
+ el = jnp.linalg.norm(itrf, axis=-1) - earth_radius(lat)
545
+
546
+ return jnp.array([lat, lon, el]).T
547
+
548
+
549
+ @jit_with_doc
550
+ def itrf_to_xyz(itrf: Array, gsa: Array) -> Array:
551
+ """Transform coordinates from the ITRF (ECEF) frame to an ECI frame that aligns with the celestial sphere.
552
+
553
+ Parameters
554
+ ----------
555
+ itrf : Array (n_ant, 3)
556
+ ITRF coordinates in metres.
557
+ gsa : Array (n_time,)
558
+ Greenwich sidereal angle in degrees.
559
+
560
+ Returns
561
+ -------
562
+ Array (n_time, n_ant, 3)
563
+ ECI coordinates in metres.
564
+ """
565
+
566
+ itrf = jnp.atleast_2d(itrf)
567
+ gsa = jnp.atleast_1d(gsa)
568
+ ecef_to_eci = lambda ecef, gsa: jnp.einsum("ij,aj->ai", Rotz(gsa), ecef)
569
+ xyz = vmap(ecef_to_eci, in_axes=(None, 0))(itrf, gsa)
570
+
571
+ return xyz
572
+
573
+
574
+ @jit_with_doc
575
+ def xyz_to_itrf(xyz: Array, gsa: Array) -> Array:
576
+ """Transform coordinates from the ECI frame to the ITRF (ECEF) frame that is fixed with the Earth.
577
+
578
+ Parameters
579
+ ----------
580
+ xyz : Array (n_time, 3)
581
+ ECI coordinates in metres.
582
+ gsa : Array (n_time,)
583
+ Greenwich sidereal time in degrees.
584
+
585
+ Returns
586
+ -------
587
+ Array (n_time, 3)
588
+ ITRF (ECEF) coordinates in metres.
589
+ """
590
+
591
+ xyz = jnp.atleast_2d(xyz)
592
+ gsa = jnp.atleast_1d(gsa)
593
+ eci_to_ecef = lambda eci, gsa: Rotz(-gsa) @ eci
594
+ itrf = vmap(eci_to_ecef, in_axes=(0, 0))(xyz, gsa)
595
+
596
+ return itrf
597
+
598
+
599
+ @jit_with_doc
600
+ def itrf_to_uvw(itrf: Array, h0: Array, dec: float) -> Array:
601
+ """
602
+ Calculate uvw coordinates from ITRF/ECEF coordinates,
603
+ source hour angle and declination. Use the Greenwich hour
604
+ angle when using true ITRF coordinates such as those produced
605
+ with 'enu_to_itrf' or provided in an MS file. Use local hour angle when using local 'xyz'
606
+ coordinates as defined in most radio interferometry textbooks
607
+ or those produced with 'enu_to_xyz_local'.
608
+
609
+ Parameters
610
+ ----------
611
+ ITRF: Array (n_ant, 3)
612
+ Antenna positions in the ITRF frame in units of metres.
613
+ h0: Array (n_time,)
614
+ The hour angle of the target in decimal degrees.
615
+ dec: float
616
+ The declination of the target in decimal degrees.
617
+
618
+ Returns
619
+ -------
620
+ uvw: Array (n_time, n_ant, 3)
621
+ The uvw coordinates of the antennas for a given observer
622
+ location, time and target (ra,dec).
623
+ """
624
+
625
+ itrf = jnp.atleast_2d(itrf)
626
+ itrf = itrf - itrf[0, None, :]
627
+
628
+ h0 = jnp.deg2rad(jnp.atleast_1d(h0))
629
+ dec = jnp.deg2rad(jnp.asarray(dec))
630
+ ones = jnp.ones_like(h0)
631
+
632
+ R = jnp.array(
633
+ [
634
+ [jnp.sin(h0), jnp.cos(h0), jnp.zeros_like(h0)],
635
+ [
636
+ -jnp.sin(dec) * jnp.cos(h0),
637
+ jnp.sin(dec) * jnp.sin(h0),
638
+ jnp.cos(dec) * ones,
639
+ ],
640
+ [
641
+ jnp.cos(dec) * jnp.cos(h0),
642
+ -jnp.cos(dec) * jnp.sin(h0),
643
+ jnp.sin(dec) * ones,
644
+ ],
645
+ ]
646
+ )
647
+
648
+ uvw = jnp.einsum("ijt,aj->tai", R, itrf)
649
+
650
+ return uvw
651
+
652
+
653
+ @jit_with_doc
654
+ def calculate_fringe_frequency(
655
+ times_mjd: Array,
656
+ freq: float,
657
+ rfi_xyz: Array,
658
+ ants_itrf: Array,
659
+ ants_u: Array,
660
+ dec: float,
661
+ ) -> Array:
662
+ """Calculate the fringe frequency of an RFI source.
663
+
664
+ Parameters
665
+ ----------
666
+ times_mjd : Array (n_time,)
667
+ Times are which the RFI and antenna positions are given in Modified Julian Date.
668
+ freq : float
669
+ Observational frequency in Hz.
670
+ rfi_xyz : Array (n_time, 3)
671
+ Position of the RFI source in the ECI frame in metres.
672
+ ants_itrf : Array (n_ant, 3)
673
+ Antenna positions in the ITRF (ECEF) frame in metres.
674
+ ants_u : Array (n_time, n_ant)
675
+ U component of the antennas in UVW frame in metres.
676
+ dec : float
677
+ Phase centre declination in degrees.
678
+
679
+ Returns
680
+ -------
681
+ Array (n_time, n_bl)
682
+ Fringe frequencies on each baseline.
683
+ """
684
+
685
+ lam = C / freq
686
+ # Should change this to astropy.time.Time
687
+ gsa = gmsa_from_jd(mjd_to_jd(times_mjd))
688
+ times = (times_mjd - times_mjd[0]) * 24 * 3600
689
+
690
+ r_ecef = xyz_to_itrf(rfi_xyz, gsa)
691
+ s_ecef = r_ecef - jnp.mean(ants_itrf, axis=0)
692
+ s_hat_ecef = s_ecef / jnp.linalg.norm(s_ecef, axis=-1, keepdims=True)
693
+ s_hat_dot = jnp.gradient(s_hat_ecef, times, axis=0)
694
+
695
+ a1, a2 = jnp.triu_indices(len(ants_itrf), 1)
696
+ bl_ecef = ants_itrf[a1] - ants_itrf[a2]
697
+ bl_u = ants_u[:, a1] - ants_u[:, a2]
698
+
699
+ fringe_move = jnp.einsum("bi,ti->tb", bl_ecef, s_hat_dot) / lam
700
+ fringe_stat = -bl_u * Omega_e * jnp.cos(jnp.deg2rad(dec)) / lam
701
+ fringe_freq = fringe_move - fringe_stat
702
+
703
+ return fringe_freq
704
+
705
+
706
+ @jit_with_doc
707
+ def calculate_sat_corr_time(
708
+ sat_xyz: Array,
709
+ ants_xyz: Array,
710
+ orbit_el: float,
711
+ lat: float,
712
+ dish_d: float,
713
+ freqs: Array,
714
+ ) -> float:
715
+ """Calculate the expected correlation time for a Gaussian process model of the RFI signal due to a satellite moving through the sidelobes of the primary beam.
716
+
717
+ Parameters
718
+ ----------
719
+ rfi_xyz : Array (n_time, 3)
720
+ Position of the RFI source over time in metres
721
+ ants_xyz : Array (n_time, n_ant, 3)
722
+ Positions of the antennas over time in metres.
723
+ orbit_el : float
724
+ Orbit elevation in metres.
725
+ lat : float
726
+ Latitude of the antennas in degrees.
727
+ dish_d : float
728
+ Dish diameter in metres. The primary beam is modelled by an Airy disk based on this.
729
+ freqs : Array (n_freq,)
730
+ Observational frequencies in Hz.
731
+
732
+ Returns
733
+ -------
734
+ float
735
+ Expected minimum correlation time in seconds.
736
+ """
737
+
738
+ r = sat_xyz - ants_xyz.mean(axis=1)
739
+ R = jnp.linalg.norm(r, axis=-1).min()
740
+
741
+ R_sat = earth_radius(lat) + orbit_el
742
+ v_sat = jnp.sqrt(G * M_e / R_sat)
743
+
744
+ l = jnp.min(C / freqs) * R / (dish_d * v_sat) / 4
745
+
746
+ return l
747
+
748
+
749
+ @jit_with_doc
750
+ def angular_separation(rfi_xyz: Array, ants_xyz: Array, ra: float, dec: float) -> Array:
751
+ """
752
+ Calculate the angular separation between the pointing direction of each
753
+ antenna and the satellite source.
754
+
755
+ Parameters
756
+ ----------
757
+ rfi_xyz: ndarray (n_src, n_time, 3)
758
+ Position of the RFI sources in ECEF reference frame over time. This
759
+ is the same frame as `ants_xyz`.
760
+ ants_xyz: ndarray (n_time, n_ant, 3)
761
+ Position of the antennas in ECEF reference frame over time.
762
+ ra: float
763
+ Right ascension of the pointing in decimal degrees.
764
+ dec: float
765
+ Declination of the pointing in decimal degrees.
766
+
767
+ Returns
768
+ -------
769
+ angles: ndarray (n_src, n_time, n_ant)
770
+ Angular separation (in degrees) between the pointing direction and RFI source for each antenna.
771
+ """
772
+ rfi_xyz = jnp.asarray(rfi_xyz)
773
+ ants_xyz = jnp.asarray(ants_xyz)
774
+ ra = jnp.asarray(ra).flatten()[0]
775
+ dec = jnp.asarray(dec).flatten()[0]
776
+ src_xyz = radec_to_XYZ(ra, dec)
777
+ ant_to_sat_xyz = rfi_xyz[:, :, None, :] - ants_xyz[None, :, :, :]
778
+ costheta = jnp.einsum("i,ljki->ljk", src_xyz, ant_to_sat_xyz) / jnp.linalg.norm(
779
+ ant_to_sat_xyz, axis=-1
780
+ )
781
+ angles = jnp.rad2deg(jnp.arccos(costheta))
782
+ return angles
783
+
784
+
785
+ @jit_with_doc
786
+ def Rotx(theta: float) -> Array:
787
+ """
788
+ Define a rotation matrix about the 'x-axis' by an angle theta, in degrees.
789
+
790
+ Parameters
791
+ ----------
792
+ theta: float
793
+ Rotation angle in degrees.
794
+
795
+ Returns
796
+ -------
797
+ R: ndarray (3, 3)
798
+ Rotation matrix.
799
+ """
800
+ theta = jnp.asarray(theta).flatten()[0]
801
+ c = jnp.cos(jnp.deg2rad(theta))
802
+ s = jnp.sin(jnp.deg2rad(theta))
803
+ Rx = jnp.array([[1, 0, 0], [0, c, -s], [0, s, c]])
804
+
805
+ return Rx
806
+
807
+
808
+ @jit_with_doc
809
+ def Rotz(theta: float) -> Array:
810
+ """
811
+ Define a rotation matrix about the 'z-axis' by an angle theta, in degrees.
812
+
813
+ Parameters
814
+ ----------
815
+ theta: float
816
+ Rotation angle in degrees.
817
+
818
+ Returns
819
+ -------
820
+ R: ndarray (3, 3)
821
+ Rotation matrix.
822
+ """
823
+ theta = jnp.asarray(theta).flatten()[0]
824
+ c = jnp.cos(jnp.deg2rad(theta))
825
+ s = jnp.sin(jnp.deg2rad(theta))
826
+ Rz = jnp.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
827
+
828
+ return Rz
829
+
830
+
831
+ @jit_with_doc
832
+ def orbit(
833
+ times: Array,
834
+ elevation: float,
835
+ inclination: float,
836
+ lon_asc_node: float,
837
+ periapsis: float,
838
+ ) -> Array:
839
+ """
840
+ Calculate orbital path of a satellite in perfect circular orbit.
841
+
842
+ Parameters
843
+ ----------
844
+ times: ndarray (n_time,)
845
+ Times at which to evaluate the positions.
846
+ elevation: float
847
+ Elevation/Altitude of the orbit in metres.
848
+ inclination: float
849
+ Inclination angle of the orbit relative to the equatorial plane.
850
+ lon_asc_node: float
851
+ Longitude of the ascending node of the orbit. This is the longitude of
852
+ when the orbit crosses the equator from the south to the north.
853
+ periapsis: float
854
+ Perisapsis of the orbit. This is the angular starting point of the
855
+ orbit at t = 0.
856
+
857
+ Returns
858
+ -------
859
+ positions: ndarray (n_time, 3)
860
+ The position vector of the orbiting object at each specified time.
861
+ """
862
+ times = jnp.asarray(times)
863
+ elevation = jnp.asarray(elevation).flatten()[0]
864
+ inclination = jnp.asarray(inclination).flatten()[0]
865
+ lon_asc_node = jnp.asarray(lon_asc_node).flatten()[0]
866
+ periapsis = jnp.asarray(periapsis).flatten()[0]
867
+ R = R_e + elevation
868
+ omega = jnp.sqrt(G * M_e / R**3)
869
+ r = R * jnp.array(
870
+ [jnp.cos(omega * (times)), jnp.sin(omega * (times)), jnp.zeros(len(times))]
871
+ )
872
+ R1 = Rotz(periapsis)
873
+ R2 = Rotx(inclination)
874
+ R3 = Rotz(lon_asc_node)
875
+ rt = (R3 @ R2 @ R1 @ r).T
876
+
877
+ return rt
878
+
879
+
880
+ def orbit_vmap(
881
+ times: Array,
882
+ elevation: Array,
883
+ inclination: Array,
884
+ lon_asc_node: Array,
885
+ periapsis: Array,
886
+ ) -> Array:
887
+ """Calculate orbital path of a satellite in perfect circular orbit.
888
+
889
+ Parameters
890
+ ----------
891
+ times: ndarray (n_time,)
892
+ Times at which to evaluate the positions.
893
+ elevation: nd_array (n_orbits,)
894
+ Elevation/Altitude of the orbit in metres.
895
+ inclination: nd_array (n_orbits,)
896
+ Inclination angle of the orbit relative to the equatorial plane.
897
+ lon_asc_node: nd_array (n_orbits,)
898
+ Longitude of the ascending node of the orbit. This is the longitude of
899
+ when the orbit crosses the equator from the south to the north.
900
+ periapsis: nd_array (n_orbits,)
901
+ Perisapsis of the orbit. This is the angular starting point of the
902
+ orbit at t = 0.
903
+
904
+ Returns
905
+ -------
906
+ positions: ndarray (n_orbits, n_time, 3)
907
+ The position vectors of the orbiting objects at each specified time.
908
+ """
909
+ return vmap(orbit, in_axes=(None, 0, 0, 0, 0))(
910
+ times, elevation, inclination, lon_asc_node, periapsis
911
+ )
912
+
913
+
914
+ @jit_with_doc
915
+ def orbit_velocity(
916
+ times: Array,
917
+ elevation: float,
918
+ inclination: float,
919
+ lon_asc_node: float,
920
+ periapsis: float,
921
+ ) -> Array:
922
+ """
923
+ Calculate the velocity of a circular orbit at specific times.
924
+
925
+ Parameters
926
+ ----------
927
+ times: ndarray (n_time,)
928
+ Times at which to evaluate the rotation matrices.
929
+ elevation: float
930
+ Elevation/Altitude of the orbit in metres.
931
+ inclination: float
932
+ Inclination angle of the orbit relative to the equatorial plane.
933
+ lon_asc_node: float
934
+ Longitude of the ascending node of the orbit. This is the longitude of
935
+ when the orbit crosses the equator from the south to the north.
936
+ periapsis: float
937
+ Perisapsis of the orbit. This is the angular starting point of the orbit
938
+ at t = 0.
939
+
940
+ Returns
941
+ -------
942
+ velocity: ndarray (n_time, 3)
943
+ The velocity vector at the specified times.
944
+ """
945
+ times = jnp.asarray(times)
946
+ elevation = jnp.asarray(elevation)
947
+ inclination = jnp.asarray(inclination)
948
+ lon_asc_node = jnp.asarray(lon_asc_node)
949
+ periapsis = jnp.asarray(periapsis)
950
+ vel_vmap = vmap(jacrev(orbit, argnums=(0)), in_axes=(0, None, None, None, None))
951
+ velocity = vel_vmap(
952
+ times[:, None], elevation, inclination, lon_asc_node, periapsis
953
+ ).reshape(len(times), 3)
954
+
955
+ return velocity
956
+
957
+
958
+ @jit_with_doc
959
+ def R_uvw(
960
+ times: Array,
961
+ elevation: float,
962
+ inclination: float,
963
+ lon_asc_node: float,
964
+ periapsis: float,
965
+ ) -> Array:
966
+ """
967
+ Calculate the rotation matrices at each time step to transform from an Earth
968
+ centric reference frame (ECI) to a satellite centric frame given the
969
+ parameters of its (circular) orbit.
970
+
971
+ Parameters
972
+ ----------
973
+ times: ndarray (n_time,)
974
+ Times at which to evaluate the rotation matrices.
975
+ elevation: float
976
+ Elevation/Altitude of the orbit in metres.
977
+ inclination: float
978
+ Inclination angle of the orbit relative to the equatorial plane.
979
+ lon_asc_node: float
980
+ Longitude of the ascending node of the orbit. This is the longitude of
981
+ when the orbit crosses the equator from the south to the north.
982
+ periapsis: float
983
+ Perisapsis of the orbit. This is the angular starting point of the orbit
984
+ at t = 0.
985
+
986
+ Returns
987
+ -------
988
+ R: ndarray (3, 3)
989
+ The rotation matrix to orient to a satellite centric (RIC) frame defined
990
+ by the Radial, In-track, Cross-track components.
991
+ """
992
+ times = jnp.asarray(times)
993
+ elevation = jnp.asarray(elevation)
994
+ inclination = jnp.asarray(inclination)
995
+ lon_asc_node = jnp.asarray(lon_asc_node)
996
+ periapsis = jnp.asarray(periapsis)
997
+ position = orbit(times, elevation, inclination, lon_asc_node, periapsis)
998
+ velocity = orbit_velocity(times, elevation, inclination, lon_asc_node, periapsis)
999
+ vel_hat = velocity / jnp.linalg.norm(velocity, axis=-1, keepdims=True)
1000
+ u_hat = position / jnp.linalg.norm(position, axis=-1, keepdims=True)
1001
+ w_hat = jnp.cross(u_hat, vel_hat)
1002
+ v_hat = jnp.cross(w_hat, u_hat)
1003
+ R = jnp.stack([u_hat.T, v_hat.T, w_hat.T])
1004
+
1005
+ return R
1006
+
1007
+
1008
+ @jit_with_doc
1009
+ def RIC_dev(
1010
+ times: Array,
1011
+ true_orbit_params: Array,
1012
+ estimated_orbit_params: Array,
1013
+ ) -> Array:
1014
+ """
1015
+ Calculate the Radial (R), In-track (I) and Cross-track (C) deviations
1016
+ between two circular orbits given their orbit parameters at many time steps.
1017
+
1018
+ Parameters
1019
+ ----------
1020
+ times: array (n_time,)
1021
+ Times at which to evaluate the RIC deviations.
1022
+ true_orb_params: ndarray (4,)
1023
+ Orbit parameters (elevation, inclination, lon_asc_node, periapsis) of
1024
+ the object of most interest. The more accurate measurement when
1025
+ comparing the same object.
1026
+ est_orb_params: array (elevation, inclination, lon_asc_node, periapsis)
1027
+ Estimated orbit parameters.
1028
+
1029
+ Returns
1030
+ -------
1031
+ RIC: ndarray (n_time, 3)
1032
+ Radial, In-track and Cross-track coordinates, in metres, of the orbiting
1033
+ body relative to the orbit defined by 'true_orbit_params'.
1034
+ """
1035
+ times = jnp.asarray(times)
1036
+ true_orbit_params = jnp.asarray(true_orbit_params)
1037
+ estimated_orbit_params = jnp.asarray(estimated_orbit_params)
1038
+ true_xyz = orbit(times, *true_orbit_params)
1039
+ est_xyz = orbit(times, *estimated_orbit_params)
1040
+ R = R_uvw(times, *true_orbit_params)
1041
+ true_uvw = vmap(lambda x, y: x @ y, in_axes=(-1, 0))(R, true_xyz)
1042
+ est_uvw = vmap(lambda x, y: x @ y, in_axes=(-1, 0))(R, est_xyz)
1043
+
1044
+ return est_uvw - true_uvw
1045
+
1046
+
1047
+ @jit_with_doc
1048
+ def orbit_fisher(times: Array, orbit_params: Array, RIC_std: Array) -> Array:
1049
+ """
1050
+ Calculate the inverse covariance (Fisher) matrix in orbital elements
1051
+ induced by errors in the RIC frame of an orbiting object. This is
1052
+ essentially linear uncertainty propagation assuming Gaussian errors.
1053
+
1054
+ Parameters
1055
+ ----------
1056
+ times: ndarray (n_time,)
1057
+ Times at which the RIC covariance/standard deviations is defined.
1058
+ orbit_params: ndarray (4,)
1059
+ Orbit parameters (elevation, inclination, lon_asc_node, periapsis) for
1060
+ the orbit defining the origin of the RIC frame.
1061
+ RIC_std: ndarray (3,)
1062
+ The standard deviation in the Radial, In-track and Cross-track
1063
+ directions.
1064
+
1065
+ Returns
1066
+ -------
1067
+ fisher: ndarray (4, 4)
1068
+ The inverse covariance (Fisher) matrix for the orbit parameters induced
1069
+ by the orbit uncertainties in the RIC frame.
1070
+ """
1071
+ times = jnp.asarray(times)
1072
+ orbit_params = jnp.asarray(orbit_params)
1073
+ RIC_std = jnp.asarray(RIC_std)
1074
+ J = jacrev(RIC_dev, argnums=(2,))(times, orbit_params, orbit_params)[0]
1075
+ F = jnp.diag(1.0 / RIC_std**2)
1076
+
1077
+ def propagate(J, F):
1078
+ return J.T @ F @ J
1079
+
1080
+ fisher = vmap(propagate, in_axes=(0, None))(J, F).sum(axis=0)
1081
+
1082
+ return fisher