nrl-tracker 1.2.0__py3-none-any.whl → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nrl-tracker
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Python port of the U.S. Naval Research Laboratory's Tracker Component Library for target tracking algorithms
5
5
  Author: Original: David F. Crouse, Naval Research Laboratory
6
6
  Maintainer: Python Port Contributors
@@ -1,4 +1,4 @@
1
- pytcl/__init__.py,sha256=LFQ_O4Uxr3PayIyh__HjIycG7ZwPGmfGELKpDmx4lfc,1893
1
+ pytcl/__init__.py,sha256=PLcxb75b02Pbp9mvJ3PZYQR_ZEhjJybFZhYJ1CQTjL8,1893
2
2
  pytcl/logging_config.py,sha256=j7Zrkal5LwUIos-_Dm3cGKUR-jMkFdSZZikJTtzTeoE,8883
3
3
  pytcl/assignment_algorithms/__init__.py,sha256=f9V-TkEVmiKYYyth4PTpDfJvA7yYV_ys6Zix-QwWIYY,2136
4
4
  pytcl/assignment_algorithms/data_association.py,sha256=tsRxWJZk9aAPmE99BKXGouEpFfZrjPjb4HXvgxFUHhU,11405
@@ -16,7 +16,8 @@ pytcl/astronomical/orbital_mechanics.py,sha256=8GssRanwTowCl6PJYqmB_SDnNznLUq5gk
16
16
  pytcl/astronomical/reference_frames.py,sha256=gkbQrhHY_EN0xt8i7m0dIJp67GgpEcDArY7J3YH-Mb8,17981
17
17
  pytcl/astronomical/relativity.py,sha256=YPsXLD-VRh-nqs1laC-wKpRO00fflm4GkyLhojPydbo,15441
18
18
  pytcl/astronomical/time_systems.py,sha256=Jg0Zaq60hc4Ts1aQtb5bK4KSZhz-uQse8gYC89Y0-TA,15243
19
- pytcl/atmosphere/__init__.py,sha256=TTVz4hAM48Xd3jr6GKrR2GAABpx2z0aWvtzb9uIQiHk,737
19
+ pytcl/atmosphere/__init__.py,sha256=swugW8rY2Jof-z_kPQ2P9vInBYjr1M69C1Q8AgN3RVo,1457
20
+ pytcl/atmosphere/ionosphere.py,sha256=1qC3hY-27pD0XcLBjU735deKYmmi6qnj2fDG1zNbTqg,14681
20
21
  pytcl/atmosphere/models.py,sha256=pMLv8D7qoFqLZrlbTHLJJULOdDdhPskJ1m7KVKLV63E,9584
21
22
  pytcl/clustering/__init__.py,sha256=bYdhC_XJEt6KUUni9bIPxaddXNEGmIJQvGkA14rK4J8,1697
22
23
  pytcl/clustering/dbscan.py,sha256=PS6QlOwHFerbZNEb3zcNhN4oNQpgOOw5y0WskQzyKIo,7364
@@ -29,7 +30,7 @@ pytcl/containers/cluster_set.py,sha256=y36D5TNzvCN6xjg6taP2SD_MC-O5iLq9ncBlHsQ5I
29
30
  pytcl/containers/covertree.py,sha256=ePIqH1-0CxSFqCwmQ_G6MXPlXs4xH0gsmoZXF8QxhDk,13271
30
31
  pytcl/containers/kd_tree.py,sha256=9CKHAzid0DZ879hut8M4dyW_976pIWNLX3uWzELPIu4,18563
31
32
  pytcl/containers/measurement_set.py,sha256=87AbdoZIUspn1yJsiMpyQ5LoEVcerUnXefXGGPtFTJg,12654
32
- pytcl/containers/rtree.py,sha256=gv2EztvPnaAXEa6OoFnOYBY1MfTwjNMYh_BCiIomHJk,15450
33
+ pytcl/containers/rtree.py,sha256=Ss1ks6xlLnNeRlKpHoWxMcgQTPhVwjT5agMeq5DaH5A,21844
33
34
  pytcl/containers/track_list.py,sha256=6q9Qgcwm-8H_JqtOCsMssF27av4XaSkhfDl-MWb1ABc,12520
34
35
  pytcl/containers/vptree.py,sha256=4tUq0ktafusU1PILZkQxi27CZryKlsHtFbym-vZYQWk,8747
35
36
  pytcl/coordinate_systems/__init__.py,sha256=jwYhu_-9AvOeP9WLG9PYtyDwfe0GjxNZ9-xCqiLymW4,3909
@@ -54,7 +55,9 @@ pytcl/dynamic_estimation/batch_estimation/__init__.py,sha256=JQ0s76Enov5a7plA4En
54
55
  pytcl/dynamic_estimation/kalman/__init__.py,sha256=yoFLj0n-NRkdZnRVL-BkHBlATk8pfZEVlsY3BhSYgKc,2387
55
56
  pytcl/dynamic_estimation/kalman/extended.py,sha256=51uhCqkZmErCx6MBfMq8eIQW8bD7n34zCe4v4dxNiMQ,10384
56
57
  pytcl/dynamic_estimation/kalman/linear.py,sha256=1Zgg9gZya0Vxs9im7sPUqLj0Luo463vS-RSa6GCReFI,12248
57
- pytcl/dynamic_estimation/kalman/square_root.py,sha256=Hw1F4_Zc7IA6Mt1WCkjx1UuLAUmNhM5vPLvueb7oRSA,26931
58
+ pytcl/dynamic_estimation/kalman/square_root.py,sha256=N7-lDml7Nw5HM5b5D11WOwG7rY1JlVoyis0ho-vk0H4,13345
59
+ pytcl/dynamic_estimation/kalman/sr_ukf.py,sha256=LeRGBSDpvSP9CyTZjEroz2Z2uueb6YpmzYricba0PDk,8640
60
+ pytcl/dynamic_estimation/kalman/ud_filter.py,sha256=fzSdcVO_P8-E2oXc32n79Rn56GI2VUmOoMDYBHw7keM,10077
58
61
  pytcl/dynamic_estimation/kalman/unscented.py,sha256=RDK6USkko9lj1K4-WYydh3_8GMZNng_PJVjfc-c_OwM,15427
59
62
  pytcl/dynamic_estimation/measurement_update/__init__.py,sha256=8rlyJwVpxf0fZj-AFo1hlewvryZRhUzcy3F8uMe6I8c,48
60
63
  pytcl/dynamic_estimation/particle_filters/__init__.py,sha256=-DRF5rVF2749suLlArmkTvVkqeMcV_mIx0eLeTj6wNU,906
@@ -76,10 +79,10 @@ pytcl/gravity/egm.py,sha256=47I8nyXNhXUKPkufXahs4JGsBcqhM-9z2xGz0X4JPmU,18422
76
79
  pytcl/gravity/models.py,sha256=rdY3Do4M1eRFO74gu3xy-bBn7tox3zM49wYbfnsIQWw,11159
77
80
  pytcl/gravity/spherical_harmonics.py,sha256=IpBh0LW4BQMzJck9Li6yveGlvYigCuXaoApRWDPsWtc,16498
78
81
  pytcl/gravity/tides.py,sha256=hef_BGewFGD7dJwg0t09Z6tfWLco_avATLuu66rnTpI,27733
79
- pytcl/magnetism/__init__.py,sha256=hE2BvberFSmimYuuwCYJ0g7ByxJAdj844vZJNkEotws,2502
82
+ pytcl/magnetism/__init__.py,sha256=pBASOzCPHNnYqUH_XDEblhGtjz50vY9uW2KS25A0zQQ,2701
80
83
  pytcl/magnetism/emm.py,sha256=5Jwl99wvdKYtx1-3LBB7x-w5KT-fqLiRg7uBW0Ai_Gw,22292
81
84
  pytcl/magnetism/igrf.py,sha256=3g0PsH8IdbwQQS28OR5XWD-g-QxvfUva7jOkKToxndQ,13384
82
- pytcl/magnetism/wmm.py,sha256=p0H7Eo02iB6nEMvGyvjsrAWOSKrIye6PGwQtNKfHaNw,15999
85
+ pytcl/magnetism/wmm.py,sha256=q7AJrpOrn1EBbWNjltPxhGEwg3P44ay1pc4dI5OIyUY,23444
83
86
  pytcl/mathematical_functions/__init__.py,sha256=zeJ1ffRRl83k2NHn3HTn-fgtFoWNPq6LCALc3xRo4Do,3767
84
87
  pytcl/mathematical_functions/basic_matrix/__init__.py,sha256=kZv3kMAEHBdVxhbyMxTyM0s-4XJP1tK6po82UsIE4tc,1318
85
88
  pytcl/mathematical_functions/basic_matrix/decompositions.py,sha256=PWJsFDiXM2T78RHdxBJZPFnl8kFbNZQpHrbpw0mhE00,12268
@@ -100,7 +103,7 @@ pytcl/mathematical_functions/signal_processing/filters.py,sha256=8Ojf4h4rfiucBXq
100
103
  pytcl/mathematical_functions/signal_processing/matched_filter.py,sha256=AahJZRZk2IIXzRL7www0n8bc0XoKabaLOe8yYNSjuDY,22893
101
104
  pytcl/mathematical_functions/special_functions/__init__.py,sha256=AJBCKj32daQxdahUQckW0bWowzOoapxni2eZnVXERdg,3859
102
105
  pytcl/mathematical_functions/special_functions/bessel.py,sha256=M0mwLQBaUXEHA8wyKReJ2D66I1v1XR7y-txAipd-WDs,14377
103
- pytcl/mathematical_functions/special_functions/debye.py,sha256=Nchjwkl1vzSL1L7nQpslb-lvT49LgTfdTIQMeSNn4vQ,6689
106
+ pytcl/mathematical_functions/special_functions/debye.py,sha256=5u-2KIQniwoVlqGSQguYhO7RcFQXtvY0aetiDiMYtQ0,9576
104
107
  pytcl/mathematical_functions/special_functions/elliptic.py,sha256=WyzBkrfZufIR5dUmCKGcxp6KNpVDrU89NGLDyRrZOqQ,7418
105
108
  pytcl/mathematical_functions/special_functions/error_functions.py,sha256=a3SS8FYAMRv1KdCmebOZL95yjvVt9gZRF2XOjHvQ9M8,6253
106
109
  pytcl/mathematical_functions/special_functions/gamma_functions.py,sha256=xXN_9SCokH10HjE8PpaPKHYVK_RZRHRAbZgR2mZYIAA,10191
@@ -145,8 +148,8 @@ pytcl/trackers/mht.py,sha256=7mwhMmja3ri2wnx7W1wueDGn2r3ArwAxJDPUJ7IZAkQ,20617
145
148
  pytcl/trackers/multi_target.py,sha256=hvt89ERhMwpcHcIJeKHnkQSKdE3_LoRiX-gbaGoo300,10516
146
149
  pytcl/trackers/single_target.py,sha256=Yy3FwaNTArMWcaod-0HVeiioNV4xLWxNDn_7ZPVqQYs,6562
147
150
  pytcl/transponders/__init__.py,sha256=5fL4u3lKCYgPLo5uFeuZbtRZkJPABntuKYGUvVgMMEI,41
148
- nrl_tracker-1.2.0.dist-info/LICENSE,sha256=rB5G4WppIIUzMOYr2N6uyYlNJ00hRJqE5tie6BMvYuE,1612
149
- nrl_tracker-1.2.0.dist-info/METADATA,sha256=TV1ddsQ8eyXE0y4Y6O5AuUR6glbTc8pkBZ7g2J10uCA,10145
150
- nrl_tracker-1.2.0.dist-info/WHEEL,sha256=pL8R0wFFS65tNSRnaOVrsw9EOkOqxLrlUPenUYnJKNo,91
151
- nrl_tracker-1.2.0.dist-info/top_level.txt,sha256=17megxcrTPBWwPZTh6jTkwTKxX7No-ZqRpyvElnnO-s,6
152
- nrl_tracker-1.2.0.dist-info/RECORD,,
151
+ nrl_tracker-1.3.0.dist-info/LICENSE,sha256=rB5G4WppIIUzMOYr2N6uyYlNJ00hRJqE5tie6BMvYuE,1612
152
+ nrl_tracker-1.3.0.dist-info/METADATA,sha256=9DRW_Wk7EPorvcFjSKjcpNIhklbfyAXEffZHD8BeS0E,10145
153
+ nrl_tracker-1.3.0.dist-info/WHEEL,sha256=pL8R0wFFS65tNSRnaOVrsw9EOkOqxLrlUPenUYnJKNo,91
154
+ nrl_tracker-1.3.0.dist-info/top_level.txt,sha256=17megxcrTPBWwPZTh6jTkwTKxX7No-ZqRpyvElnnO-s,6
155
+ nrl_tracker-1.3.0.dist-info/RECORD,,
pytcl/__init__.py CHANGED
@@ -20,7 +20,7 @@ References
20
20
  no. 5, pp. 18-27, May 2017.
21
21
  """
22
22
 
23
- __version__ = "1.2.0"
23
+ __version__ = "1.3.0"
24
24
  __author__ = "Python Port Contributors"
25
25
  __original_author__ = "David F. Crouse, Naval Research Laboratory"
26
26
 
@@ -3,8 +3,26 @@ Atmospheric models for tracking applications.
3
3
 
4
4
  This module provides standard atmosphere models used for computing
5
5
  temperature, pressure, density, and other properties at various altitudes.
6
+
7
+ Submodules
8
+ ----------
9
+ models : Standard atmosphere models (US76, ISA)
10
+ ionosphere : Ionospheric models for GPS/GNSS corrections
6
11
  """
7
12
 
13
+ from pytcl.atmosphere.ionosphere import (
14
+ DEFAULT_KLOBUCHAR,
15
+ F_L1,
16
+ F_L2,
17
+ IonosphereState,
18
+ KlobucharCoefficients,
19
+ dual_frequency_tec,
20
+ ionospheric_delay_from_tec,
21
+ klobuchar_delay,
22
+ magnetic_latitude,
23
+ scintillation_index,
24
+ simple_iri,
25
+ )
8
26
  from pytcl.atmosphere.models import G0 # Constants
9
27
  from pytcl.atmosphere.models import (
10
28
  GAMMA,
@@ -21,17 +39,30 @@ from pytcl.atmosphere.models import (
21
39
  )
22
40
 
23
41
  __all__ = [
42
+ # Atmosphere state and models
24
43
  "AtmosphereState",
25
44
  "us_standard_atmosphere_1976",
26
45
  "isa_atmosphere",
27
46
  "altitude_from_pressure",
28
47
  "mach_number",
29
48
  "true_airspeed_from_mach",
30
- # Constants
49
+ # Atmosphere constants
31
50
  "T0",
32
51
  "P0",
33
52
  "RHO0",
34
53
  "G0",
35
54
  "R",
36
55
  "GAMMA",
56
+ # Ionosphere
57
+ "IonosphereState",
58
+ "KlobucharCoefficients",
59
+ "DEFAULT_KLOBUCHAR",
60
+ "klobuchar_delay",
61
+ "dual_frequency_tec",
62
+ "ionospheric_delay_from_tec",
63
+ "simple_iri",
64
+ "magnetic_latitude",
65
+ "scintillation_index",
66
+ "F_L1",
67
+ "F_L2",
37
68
  ]
@@ -0,0 +1,512 @@
1
+ """
2
+ Ionospheric models for radio propagation and navigation applications.
3
+
4
+ This module provides ionospheric models used for computing signal delays,
5
+ electron density profiles, and Total Electron Content (TEC) estimates.
6
+ These are essential for GPS/GNSS corrections and radio wave propagation.
7
+
8
+ Models
9
+ ------
10
+ - Klobuchar: GPS broadcast ionospheric model (single-frequency correction)
11
+ - NeQuick: Galileo ionospheric model placeholder
12
+ - IRI: International Reference Ionosphere simplified model
13
+
14
+ References
15
+ ----------
16
+ .. [1] Klobuchar, J.A. (1987). "Ionospheric Time-Delay Algorithm for
17
+ Single-Frequency GPS Users". IEEE Transactions on Aerospace and
18
+ Electronic Systems, AES-23(3), 325-331.
19
+ .. [2] Nava, B., Coisson, P., & Radicella, S.M. (2008). "A new version
20
+ of the NeQuick ionosphere electron density model". Journal of
21
+ Atmospheric and Solar-Terrestrial Physics, 70(15), 1856-1862.
22
+ """
23
+
24
+ from typing import NamedTuple
25
+
26
+ import numpy as np
27
+ from numpy.typing import ArrayLike, NDArray
28
+
29
+ # Physical constants
30
+ SPEED_OF_LIGHT = 299792458.0 # m/s
31
+ F_L1 = 1575.42e6 # GPS L1 frequency (Hz)
32
+ F_L2 = 1227.60e6 # GPS L2 frequency (Hz)
33
+
34
+
35
+ class IonosphereState(NamedTuple):
36
+ """
37
+ Ionospheric state at a given location and time.
38
+
39
+ Attributes
40
+ ----------
41
+ tec : float or ndarray
42
+ Total Electron Content in TECU (10^16 electrons/m²).
43
+ delay_l1 : float or ndarray
44
+ Ionospheric delay at L1 frequency in meters.
45
+ delay_l2 : float or ndarray
46
+ Ionospheric delay at L2 frequency in meters.
47
+ f_peak : float or ndarray
48
+ Critical frequency of F2 layer in MHz.
49
+ h_peak : float or ndarray
50
+ Height of F2 layer peak in km.
51
+ """
52
+
53
+ tec: float | NDArray[np.float64]
54
+ delay_l1: float | NDArray[np.float64]
55
+ delay_l2: float | NDArray[np.float64]
56
+ f_peak: float | NDArray[np.float64]
57
+ h_peak: float | NDArray[np.float64]
58
+
59
+
60
+ class KlobucharCoefficients(NamedTuple):
61
+ """
62
+ Klobuchar ionospheric model coefficients.
63
+
64
+ These coefficients are broadcast by GPS satellites in the navigation message.
65
+
66
+ Attributes
67
+ ----------
68
+ alpha : ndarray
69
+ Amplitude coefficients (4 values) in seconds.
70
+ beta : ndarray
71
+ Period coefficients (4 values) in seconds.
72
+ """
73
+
74
+ alpha: NDArray[np.float64]
75
+ beta: NDArray[np.float64]
76
+
77
+
78
+ # Default Klobuchar coefficients (typical mid-latitude values)
79
+ DEFAULT_KLOBUCHAR = KlobucharCoefficients(
80
+ alpha=np.array([3.82e-8, 1.49e-8, -5.96e-8, -5.96e-8]),
81
+ beta=np.array([1.43e5, 0.0, -3.28e5, 1.13e5]),
82
+ )
83
+
84
+
85
+ def klobuchar_delay(
86
+ latitude: ArrayLike,
87
+ longitude: ArrayLike,
88
+ elevation: ArrayLike,
89
+ azimuth: ArrayLike,
90
+ gps_time: ArrayLike,
91
+ coefficients: KlobucharCoefficients | None = None,
92
+ ) -> NDArray[np.float64]:
93
+ """
94
+ Compute ionospheric delay using the Klobuchar model.
95
+
96
+ The Klobuchar model is the standard GPS broadcast ionospheric
97
+ correction model. It provides single-frequency ionospheric
98
+ delay estimates accurate to about 50% RMS.
99
+
100
+ Parameters
101
+ ----------
102
+ latitude : array_like
103
+ User geodetic latitude in radians.
104
+ longitude : array_like
105
+ User geodetic longitude in radians.
106
+ elevation : array_like
107
+ Satellite elevation angle in radians.
108
+ azimuth : array_like
109
+ Satellite azimuth angle in radians.
110
+ gps_time : array_like
111
+ GPS time of week in seconds.
112
+ coefficients : KlobucharCoefficients, optional
113
+ Ionospheric coefficients from GPS navigation message.
114
+ If None, uses default mid-latitude values.
115
+
116
+ Returns
117
+ -------
118
+ delay : ndarray
119
+ Ionospheric delay in meters (at L1 frequency).
120
+
121
+ Examples
122
+ --------
123
+ >>> # User at 40°N, 105°W, satellite at 45° elevation
124
+ >>> delay = klobuchar_delay(
125
+ ... np.radians(40), np.radians(-105),
126
+ ... np.radians(45), np.radians(180),
127
+ ... gps_time=43200 # Noon
128
+ ... )
129
+ >>> delay > 0
130
+ True
131
+
132
+ Notes
133
+ -----
134
+ The Klobuchar model assumes a thin-shell ionosphere at 350 km altitude
135
+ and uses a cosine model for diurnal variation. It typically removes
136
+ about 50% of the ionospheric delay.
137
+
138
+ References
139
+ ----------
140
+ .. [1] IS-GPS-200, Interface Specification.
141
+ """
142
+ latitude = np.asarray(latitude, dtype=np.float64)
143
+ longitude = np.asarray(longitude, dtype=np.float64)
144
+ elevation = np.asarray(elevation, dtype=np.float64)
145
+ azimuth = np.asarray(azimuth, dtype=np.float64)
146
+ gps_time = np.asarray(gps_time, dtype=np.float64)
147
+
148
+ if coefficients is None:
149
+ coefficients = DEFAULT_KLOBUCHAR
150
+
151
+ alpha = coefficients.alpha
152
+ beta = coefficients.beta
153
+
154
+ # Semi-circles (GPS convention)
155
+ phi_u = latitude / np.pi # User latitude in semi-circles
156
+ lam_u = longitude / np.pi # User longitude in semi-circles
157
+
158
+ # Earth's central angle (semi-circles)
159
+ psi = 0.0137 / (elevation / np.pi + 0.11) - 0.022
160
+
161
+ # Ionospheric pierce point latitude (semi-circles)
162
+ phi_i = phi_u + psi * np.cos(azimuth)
163
+ phi_i = np.clip(phi_i, -0.416, 0.416)
164
+
165
+ # Ionospheric pierce point longitude (semi-circles)
166
+ lam_i = lam_u + psi * np.sin(azimuth) / np.cos(phi_i * np.pi)
167
+
168
+ # Geomagnetic latitude (semi-circles)
169
+ phi_m = phi_i + 0.064 * np.cos((lam_i - 1.617) * np.pi)
170
+
171
+ # Local time at ionospheric pierce point (seconds)
172
+ t = 43200 * lam_i + gps_time
173
+ t = np.mod(t, 86400)
174
+
175
+ # Obliquity factor
176
+ F = 1.0 + 16.0 * (0.53 - elevation / np.pi) ** 3
177
+
178
+ # Ionospheric delay computation
179
+ # Amplitude
180
+ AMP = alpha[0] + alpha[1] * phi_m + alpha[2] * phi_m**2 + alpha[3] * phi_m**3
181
+ AMP = np.maximum(AMP, 0)
182
+
183
+ # Period
184
+ PER = beta[0] + beta[1] * phi_m + beta[2] * phi_m**2 + beta[3] * phi_m**3
185
+ PER = np.maximum(PER, 72000)
186
+
187
+ # Phase
188
+ x = 2 * np.pi * (t - 50400) / PER
189
+
190
+ # Ionospheric time delay (seconds)
191
+ delay_sec = np.where(
192
+ np.abs(x) < 1.57,
193
+ F * (5e-9 + AMP * (1 - x**2 / 2 + x**4 / 24)),
194
+ F * 5e-9,
195
+ )
196
+
197
+ # Convert to meters
198
+ delay_m = delay_sec * SPEED_OF_LIGHT
199
+
200
+ return delay_m
201
+
202
+
203
+ def dual_frequency_tec(
204
+ pseudorange_l1: ArrayLike,
205
+ pseudorange_l2: ArrayLike,
206
+ ) -> NDArray[np.float64]:
207
+ """
208
+ Compute Total Electron Content from dual-frequency pseudoranges.
209
+
210
+ This method uses the dispersive nature of the ionosphere to
211
+ estimate TEC from the difference in L1 and L2 pseudoranges.
212
+
213
+ Parameters
214
+ ----------
215
+ pseudorange_l1 : array_like
216
+ L1 pseudorange in meters.
217
+ pseudorange_l2 : array_like
218
+ L2 pseudorange in meters.
219
+
220
+ Returns
221
+ -------
222
+ tec : ndarray
223
+ Total Electron Content in TECU (10^16 electrons/m²).
224
+
225
+ Notes
226
+ -----
227
+ The ionospheric delay is proportional to TEC and inversely
228
+ proportional to frequency squared:
229
+ delay = 40.3 * TEC / f²
230
+
231
+ The difference in delays at L1 and L2 gives:
232
+ P2 - P1 = 40.3 * TEC * (1/f1² - 1/f2²)
233
+
234
+ This is the standard dual-frequency ionospheric correction method.
235
+ """
236
+ pseudorange_l1 = np.asarray(pseudorange_l1, dtype=np.float64)
237
+ pseudorange_l2 = np.asarray(pseudorange_l2, dtype=np.float64)
238
+
239
+ # Ionospheric coefficient
240
+ K = 40.3 # m³/s²
241
+
242
+ # Frequency squared terms
243
+ f1_sq = F_L1**2
244
+ f2_sq = F_L2**2
245
+
246
+ # TEC from pseudorange difference
247
+ # P2 - P1 = K * TEC * (1/f2² - 1/f1²) / 10^16
248
+ # Note: negative because f1 > f2
249
+ delta_inv_f_sq = 1 / f2_sq - 1 / f1_sq
250
+ tec = (pseudorange_l2 - pseudorange_l1) / (K * delta_inv_f_sq) / 1e16
251
+
252
+ return tec
253
+
254
+
255
+ def ionospheric_delay_from_tec(
256
+ tec: ArrayLike,
257
+ frequency: float = F_L1,
258
+ ) -> NDArray[np.float64]:
259
+ """
260
+ Compute ionospheric delay from Total Electron Content.
261
+
262
+ Parameters
263
+ ----------
264
+ tec : array_like
265
+ Total Electron Content in TECU (10^16 electrons/m²).
266
+ frequency : float, optional
267
+ Signal frequency in Hz. Default is GPS L1.
268
+
269
+ Returns
270
+ -------
271
+ delay : ndarray
272
+ Ionospheric delay in meters.
273
+
274
+ Notes
275
+ -----
276
+ The ionospheric delay for a signal is:
277
+ delay = 40.3 * TEC * 10^16 / f²
278
+ """
279
+ tec = np.asarray(tec, dtype=np.float64)
280
+
281
+ K = 40.3 # m³/s²
282
+ delay = K * tec * 1e16 / frequency**2
283
+
284
+ return delay
285
+
286
+
287
+ def simple_iri(
288
+ latitude: ArrayLike,
289
+ longitude: ArrayLike,
290
+ altitude: ArrayLike,
291
+ hour: ArrayLike,
292
+ month: int = 6,
293
+ solar_flux: float = 150.0,
294
+ ) -> IonosphereState:
295
+ """
296
+ Simplified International Reference Ionosphere (IRI) model.
297
+
298
+ This provides approximate electron density and TEC values based on
299
+ simplified IRI physics. For accurate predictions, use the full IRI
300
+ model or external services.
301
+
302
+ Parameters
303
+ ----------
304
+ latitude : array_like
305
+ Geodetic latitude in radians.
306
+ longitude : array_like
307
+ Geodetic longitude in radians.
308
+ altitude : array_like
309
+ Altitude in meters.
310
+ hour : array_like
311
+ Local hour (0-24).
312
+ month : int, optional
313
+ Month of year (1-12). Default is 6 (June).
314
+ solar_flux : float, optional
315
+ F10.7 solar flux in SFU. Default is 150 (moderate activity).
316
+
317
+ Returns
318
+ -------
319
+ state : IonosphereState
320
+ Ionospheric state with TEC, delays, and F2 layer parameters.
321
+
322
+ Notes
323
+ -----
324
+ This is a simplified empirical model suitable for educational purposes
325
+ and rough estimates. For operational use, the full IRI-2020 model
326
+ should be employed.
327
+
328
+ Examples
329
+ --------
330
+ >>> state = simple_iri(np.radians(40), np.radians(-105), 300e3, 12)
331
+ >>> state.tec > 0
332
+ True
333
+ """
334
+ latitude = np.asarray(latitude, dtype=np.float64)
335
+ longitude = np.asarray(longitude, dtype=np.float64)
336
+ altitude = np.asarray(altitude, dtype=np.float64)
337
+ hour = np.asarray(hour, dtype=np.float64)
338
+
339
+ # Convert latitude to degrees for calculations
340
+ lat_deg = np.degrees(latitude)
341
+ # lon_deg not used in simplified model but kept for future expansion
342
+
343
+ # Simplified F2 layer critical frequency (foF2) model
344
+ # Based on typical diurnal and latitudinal variations
345
+ lat_factor = np.cos(latitude) ** 0.8
346
+ hour_angle = 2 * np.pi * (hour - 14) / 24 # Peak around 14:00 local
347
+ diurnal = 0.5 * (1 + np.cos(hour_angle))
348
+
349
+ # Solar activity factor
350
+ solar_factor = 0.5 + 0.5 * (solar_flux - 70) / 180
351
+ solar_factor = np.clip(solar_factor, 0.3, 1.2)
352
+
353
+ # Seasonal factor (simplified)
354
+ season_angle = 2 * np.pi * (month - 1) / 12
355
+ season_factor = 1.0 + 0.2 * np.cos(season_angle - np.pi * np.sign(lat_deg))
356
+
357
+ # F2 layer critical frequency (MHz)
358
+ f_peak = 5.0 + 8.0 * lat_factor * diurnal * solar_factor * season_factor
359
+ f_peak = np.maximum(f_peak, 2.0)
360
+
361
+ # F2 layer peak height (km)
362
+ h_peak = 250 + 100 * (1 - lat_factor) + 50 * (1 - diurnal)
363
+
364
+ # Simplified TEC calculation (TECU)
365
+ # TEC roughly scales with foF2 squared
366
+ base_tec = 0.5 * f_peak**2
367
+ tec = base_tec * solar_factor * season_factor
368
+
369
+ # Ionospheric delays
370
+ delay_l1 = ionospheric_delay_from_tec(tec, F_L1)
371
+ delay_l2 = ionospheric_delay_from_tec(tec, F_L2)
372
+
373
+ # Handle scalar vs array output
374
+ if np.ndim(latitude) == 0:
375
+ return IonosphereState(
376
+ tec=float(tec),
377
+ delay_l1=float(delay_l1),
378
+ delay_l2=float(delay_l2),
379
+ f_peak=float(f_peak),
380
+ h_peak=float(h_peak),
381
+ )
382
+
383
+ return IonosphereState(
384
+ tec=tec,
385
+ delay_l1=delay_l1,
386
+ delay_l2=delay_l2,
387
+ f_peak=f_peak,
388
+ h_peak=h_peak,
389
+ )
390
+
391
+
392
+ def magnetic_latitude(
393
+ latitude: ArrayLike,
394
+ longitude: ArrayLike,
395
+ ) -> NDArray[np.float64]:
396
+ """
397
+ Compute approximate geomagnetic latitude.
398
+
399
+ Uses a simple dipole approximation with the magnetic pole at
400
+ approximately 80.5°N, 72.8°W.
401
+
402
+ Parameters
403
+ ----------
404
+ latitude : array_like
405
+ Geodetic latitude in radians.
406
+ longitude : array_like
407
+ Geodetic longitude in radians.
408
+
409
+ Returns
410
+ -------
411
+ mag_lat : ndarray
412
+ Geomagnetic latitude in radians.
413
+ """
414
+ latitude = np.asarray(latitude, dtype=np.float64)
415
+ longitude = np.asarray(longitude, dtype=np.float64)
416
+
417
+ # Approximate magnetic pole location (2020 epoch)
418
+ pole_lat = np.radians(80.5)
419
+ pole_lon = np.radians(-72.8)
420
+
421
+ # Spherical law of cosines for angular distance
422
+ cos_mag_lat = np.sin(latitude) * np.sin(pole_lat) + np.cos(latitude) * np.cos(
423
+ pole_lat
424
+ ) * np.cos(longitude - pole_lon)
425
+
426
+ # Geomagnetic colatitude
427
+ mag_colat = np.arccos(np.clip(cos_mag_lat, -1, 1))
428
+
429
+ # Geomagnetic latitude
430
+ mag_lat = np.pi / 2 - mag_colat
431
+
432
+ return mag_lat
433
+
434
+
435
+ def scintillation_index(
436
+ magnetic_latitude: ArrayLike,
437
+ hour: ArrayLike,
438
+ kp_index: float = 3.0,
439
+ ) -> NDArray[np.float64]:
440
+ """
441
+ Estimate ionospheric scintillation index S4.
442
+
443
+ Provides a rough estimate of amplitude scintillation based on
444
+ geomagnetic latitude, local time, and geomagnetic activity.
445
+
446
+ Parameters
447
+ ----------
448
+ magnetic_latitude : array_like
449
+ Geomagnetic latitude in radians.
450
+ hour : array_like
451
+ Local hour (0-24).
452
+ kp_index : float, optional
453
+ Kp geomagnetic activity index (0-9). Default is 3 (moderate).
454
+
455
+ Returns
456
+ -------
457
+ s4 : ndarray
458
+ S4 amplitude scintillation index (0-1).
459
+
460
+ Notes
461
+ -----
462
+ S4 > 0.3 indicates moderate scintillation.
463
+ S4 > 0.6 indicates strong scintillation that may affect receivers.
464
+ """
465
+ magnetic_latitude = np.asarray(magnetic_latitude, dtype=np.float64)
466
+ hour = np.asarray(hour, dtype=np.float64)
467
+
468
+ # Scintillation is most intense:
469
+ # - At equatorial latitudes (within ±20° of magnetic equator)
470
+ # - At high latitudes (auroral zone, |lat| > 60°)
471
+ # - Post-sunset to midnight (local time 19-24)
472
+ # - During high geomagnetic activity
473
+
474
+ mag_lat_deg = np.abs(np.degrees(magnetic_latitude))
475
+
476
+ # Equatorial contribution
477
+ equatorial = np.exp(-((mag_lat_deg - 15) ** 2) / 200)
478
+
479
+ # Auroral contribution
480
+ auroral = np.exp(-((mag_lat_deg - 70) ** 2) / 100)
481
+
482
+ # Combined latitude factor
483
+ lat_factor = np.maximum(equatorial, 0.3 * auroral)
484
+
485
+ # Local time factor (peak at ~20:00 local)
486
+ hour_angle = 2 * np.pi * (hour - 20) / 24
487
+ time_factor = 0.5 * (1 + np.cos(hour_angle))
488
+
489
+ # Geomagnetic activity factor
490
+ kp_factor = 0.3 + 0.7 * (kp_index / 9)
491
+
492
+ # S4 estimate
493
+ s4 = 0.8 * lat_factor * time_factor * kp_factor
494
+
495
+ return np.clip(s4, 0, 1)
496
+
497
+
498
+ __all__ = [
499
+ "IonosphereState",
500
+ "KlobucharCoefficients",
501
+ "DEFAULT_KLOBUCHAR",
502
+ "klobuchar_delay",
503
+ "dual_frequency_tec",
504
+ "ionospheric_delay_from_tec",
505
+ "simple_iri",
506
+ "magnetic_latitude",
507
+ "scintillation_index",
508
+ # Constants
509
+ "SPEED_OF_LIGHT",
510
+ "F_L1",
511
+ "F_L2",
512
+ ]