pygnss 2.1.2__cp314-cp314t-macosx_11_0_arm64.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.
- pygnss/__init__.py +1 -0
- pygnss/_c_ext/src/constants.c +36 -0
- pygnss/_c_ext/src/hatanaka.c +94 -0
- pygnss/_c_ext/src/helpers.c +17 -0
- pygnss/_c_ext/src/klobuchar.c +313 -0
- pygnss/_c_ext/src/mtable_init.c +50 -0
- pygnss/_c_ext.cpython-314t-darwin.so +0 -0
- pygnss/cl.py +148 -0
- pygnss/constants.py +4 -0
- pygnss/decorator.py +47 -0
- pygnss/file.py +36 -0
- pygnss/filter/__init__.py +77 -0
- pygnss/filter/ekf.py +80 -0
- pygnss/filter/models.py +74 -0
- pygnss/filter/particle.py +484 -0
- pygnss/filter/ukf.py +322 -0
- pygnss/geodetic.py +1177 -0
- pygnss/gnss/__init__.py +0 -0
- pygnss/gnss/edit.py +66 -0
- pygnss/gnss/observables.py +43 -0
- pygnss/gnss/residuals.py +43 -0
- pygnss/gnss/types.py +359 -0
- pygnss/hatanaka.py +70 -0
- pygnss/ionex.py +410 -0
- pygnss/iono/__init__.py +47 -0
- pygnss/iono/chapman.py +35 -0
- pygnss/iono/gim.py +131 -0
- pygnss/logger.py +70 -0
- pygnss/nequick.py +57 -0
- pygnss/orbit/__init__.py +0 -0
- pygnss/orbit/kepler.py +63 -0
- pygnss/orbit/tle.py +186 -0
- pygnss/parsers/rtklib/stats.py +166 -0
- pygnss/rinex.py +2161 -0
- pygnss/sinex.py +121 -0
- pygnss/stats.py +75 -0
- pygnss/tensorial.py +50 -0
- pygnss/time.py +350 -0
- pygnss-2.1.2.dist-info/METADATA +129 -0
- pygnss-2.1.2.dist-info/RECORD +44 -0
- pygnss-2.1.2.dist-info/WHEEL +6 -0
- pygnss-2.1.2.dist-info/entry_points.txt +8 -0
- pygnss-2.1.2.dist-info/licenses/LICENSE +21 -0
- pygnss-2.1.2.dist-info/top_level.txt +1 -0
pygnss/ionex.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import nequick
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .iono import gim
|
|
12
|
+
from .decorator import read_contents
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load(filename: str, gim_handler: gim.GimHandler):
|
|
16
|
+
"""
|
|
17
|
+
Load an IONEX file and process its contents using the provided GIM handler.
|
|
18
|
+
|
|
19
|
+
:param filename: The path to the IONEX file to load.
|
|
20
|
+
:param gim_handler: An instance of a GIM handler to process the GIMs read from the file.
|
|
21
|
+
:return: The result of processing the IONEX file.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
return _load(filename, gim_handler)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write(filename: str, gims: List[gim.Gim], gim_type: gim.GimType,
|
|
28
|
+
pgm: str = "pygnss", runby: str = "pygnss", comment_lines: List[str] = []) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Write a list of GIMs to an IONEX file.
|
|
31
|
+
|
|
32
|
+
:param filename: The path to the IONEX file to write.
|
|
33
|
+
:param gims: A list of GIM objects to write to the file.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
EXPONENT = -1
|
|
37
|
+
FACTOR = math.pow(10, EXPONENT)
|
|
38
|
+
|
|
39
|
+
if not gims:
|
|
40
|
+
raise ValueError("The list of GIMs is empty. Cannot write to the IONEX file.")
|
|
41
|
+
|
|
42
|
+
# Extract latitudes and longitudes from the first GIM
|
|
43
|
+
latitudes = gims[0].latitudes
|
|
44
|
+
longitudes = gims[0].longitudes
|
|
45
|
+
|
|
46
|
+
lon1 = longitudes[0]
|
|
47
|
+
lon2 = longitudes[-1]
|
|
48
|
+
dlon = longitudes[1] - longitudes[0]
|
|
49
|
+
|
|
50
|
+
# Ensure all GIMs have the same latitudes and longitudes
|
|
51
|
+
for gim_obj in gims:
|
|
52
|
+
if np.array_equal(gim_obj.latitudes, latitudes) == False or \
|
|
53
|
+
np.array_equal(gim_obj.longitudes, longitudes) == False:
|
|
54
|
+
raise ValueError("All GIMs must have the same latitudes and longitudes.")
|
|
55
|
+
|
|
56
|
+
# Sort the IONEX files by epoch
|
|
57
|
+
gims.sort(key=lambda gim: gim.epoch)
|
|
58
|
+
|
|
59
|
+
first_epoch = gims[0].epoch
|
|
60
|
+
last_epoch = gims[-1].epoch
|
|
61
|
+
n_maps = len(gims)
|
|
62
|
+
|
|
63
|
+
lat_0 = gims[0].latitudes[0]
|
|
64
|
+
lat_1 = gims[0].latitudes[-1]
|
|
65
|
+
dlat = gims[0].latitudes[1] - gims[0].latitudes[0]
|
|
66
|
+
|
|
67
|
+
# We will print the map from North to South, therefore check if the
|
|
68
|
+
# latitudes need to be reversed
|
|
69
|
+
latitude_reversal = lat_0 < lat_1
|
|
70
|
+
if latitude_reversal:
|
|
71
|
+
lat_0 = gims[0].latitudes[-1]
|
|
72
|
+
lat_1 = gims[0].latitudes[0]
|
|
73
|
+
dlat = gims[0].latitudes[0] - gims[0].latitudes[1]
|
|
74
|
+
|
|
75
|
+
lon_0 = gims[0].longitudes[0]
|
|
76
|
+
lon_1 = gims[0].longitudes[-1]
|
|
77
|
+
dlon = gims[0].longitudes[1] - gims[0].longitudes[0]
|
|
78
|
+
|
|
79
|
+
doc = ""
|
|
80
|
+
|
|
81
|
+
# Header
|
|
82
|
+
today = datetime.datetime.now()
|
|
83
|
+
epoch_str = today.strftime('%d-%b-%y %H:%M')
|
|
84
|
+
|
|
85
|
+
doc +=" 1.0 IONOSPHERE MAPS NEQUICK IONEX VERSION / TYPE\n"
|
|
86
|
+
doc +=f"{pgm[:20]:<20}{runby[:20]:<20}{epoch_str[:20]:<20}PGM / RUN BY / DATE\n"
|
|
87
|
+
|
|
88
|
+
for comment_line in comment_lines:
|
|
89
|
+
doc += f"{comment_line[:60]:<60}COMMENT\n"
|
|
90
|
+
|
|
91
|
+
doc += first_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF FIRST MAP\n")
|
|
92
|
+
doc += last_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF LAST MAP\n")
|
|
93
|
+
doc += " 0 INTERVAL\n"
|
|
94
|
+
doc += f"{n_maps:>6} # OF MAPS IN FILE\n"
|
|
95
|
+
doc += " NONE MAPPING FUNCTION\n"
|
|
96
|
+
doc += " 0.0 ELEVATION CUTOFF\n"
|
|
97
|
+
doc += " OBSERVABLES USED\n"
|
|
98
|
+
doc += " 6371.0 BASE RADIUS\n"
|
|
99
|
+
doc += " 2 MAP DIMENSION\n"
|
|
100
|
+
doc += " 450.0 450.0 0.0 HGT1 / HGT2 / DHGT\n"
|
|
101
|
+
doc += f" {lat_0:6.1f}{lat_1:6.1f}{dlat:6.1f} LAT1 / LAT2 / DLAT\n"
|
|
102
|
+
doc += f" {lon_0:6.1f}{lon_1:6.1f}{dlon:6.1f} LON1 / LON2 / DLON\n"
|
|
103
|
+
doc += f"{EXPONENT:>6} EXPONENT\n"
|
|
104
|
+
doc += " END OF HEADER\n"
|
|
105
|
+
|
|
106
|
+
# Write each GIM
|
|
107
|
+
for i_map, gim_obj in enumerate(gims):
|
|
108
|
+
|
|
109
|
+
doc += f"{i_map+1:>6} START OF {gim_type.name} MAP\n"
|
|
110
|
+
|
|
111
|
+
# Write the epoch
|
|
112
|
+
epoch = gim_obj.epoch
|
|
113
|
+
doc += epoch.strftime(" %Y %m %d %H %M %S EPOCH OF CURRENT MAP\n")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
for i, _ in enumerate(latitudes):
|
|
117
|
+
|
|
118
|
+
if latitude_reversal:
|
|
119
|
+
i = len(latitudes) - 1 - i
|
|
120
|
+
|
|
121
|
+
lat = latitudes[i]
|
|
122
|
+
|
|
123
|
+
doc += f" {lat:6.1f}{lon1:6.1f}{lon2:6.1f}{dlon:6.1f} 450.0 LAT/LON1/LON2/DLON/H\n"
|
|
124
|
+
|
|
125
|
+
lat_row = gim_obj.vtec_values[i]
|
|
126
|
+
for j in range(0, len(longitudes), 16):
|
|
127
|
+
doc += "".join(f"{round(vtec / FACTOR):5d}" for vtec in lat_row[j:j+16]) + "\n"
|
|
128
|
+
|
|
129
|
+
doc += f"{i_map+1:>6} END OF {gim_type.name} MAP\n"
|
|
130
|
+
|
|
131
|
+
# Tail
|
|
132
|
+
doc += " END OF FILE\n"
|
|
133
|
+
|
|
134
|
+
with open(filename, "wt") as fh:
|
|
135
|
+
fh.write(doc)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def diff(filename_lhs: str, filename_rhs: str, output_file: str, pgm="pygnss.ionex") -> None:
|
|
140
|
+
"""
|
|
141
|
+
Compute the difference between two IONEX files and write the result in IONEX format
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
gim_handler_lhs = gim.GimHandlerArray()
|
|
145
|
+
gim_handler_rhs = gim.GimHandlerArray()
|
|
146
|
+
|
|
147
|
+
load(filename_lhs, gim_handler=gim_handler_lhs)
|
|
148
|
+
load(filename_rhs, gim_handler=gim_handler_rhs)
|
|
149
|
+
|
|
150
|
+
gim_diffs = gim.subtract_gims(gim_handler_lhs.vtec_gims, gim_handler_rhs.vtec_gims)
|
|
151
|
+
|
|
152
|
+
comment_lines = [
|
|
153
|
+
"This IONEX file contains the differences of VTEC values,",
|
|
154
|
+
"computed as vtec_left - vtec_right, where:",
|
|
155
|
+
f"- vtec_left: {os.path.basename(filename_lhs)}",
|
|
156
|
+
f"- vtec_right: {os.path.basename(filename_rhs)}",
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
write(output_file, gim_diffs, gim.GimType.TEC, pgm=pgm, comment_lines=comment_lines)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@read_contents
|
|
163
|
+
def _load(doc: str, gim_handler: gim.GimHandler):
|
|
164
|
+
"""
|
|
165
|
+
Parse the contents of an IONEX file and process each GIM using the provided handler.
|
|
166
|
+
|
|
167
|
+
:param doc: The contents of the IONEX file as a string.
|
|
168
|
+
:param gim_handler: An instance of a GIM handler to process the GIMs read from the file.
|
|
169
|
+
:raises ValueError: If the file is not a valid IONEX file or contains unsupported features.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
lines = doc.splitlines()
|
|
173
|
+
n_lines = len(lines)
|
|
174
|
+
i_body = 0
|
|
175
|
+
|
|
176
|
+
latitudes_deg: List[float] = []
|
|
177
|
+
longitudes_deg: List[float] = []
|
|
178
|
+
|
|
179
|
+
header_mark_found = False
|
|
180
|
+
|
|
181
|
+
# Header
|
|
182
|
+
for i in range(n_lines):
|
|
183
|
+
|
|
184
|
+
line = lines[i]
|
|
185
|
+
|
|
186
|
+
if line[60:].startswith('IONEX VERSION / TYPE'):
|
|
187
|
+
header_mark_found = True
|
|
188
|
+
|
|
189
|
+
elif line[60:].startswith('HGT1 / HGT2 / DHGT'):
|
|
190
|
+
_hgt1, _hgt2, _dhgt = [float(v) for v in line.split()[:3]]
|
|
191
|
+
if _dhgt != 0.0:
|
|
192
|
+
raise ValueError('Multi-layer Ionex files not supported')
|
|
193
|
+
|
|
194
|
+
elif line[60:].startswith('LAT1 / LAT2 / DLAT'):
|
|
195
|
+
_lat1, _lat2, _dlat = [float(v) for v in line.split()[:3]]
|
|
196
|
+
latitudes_deg = np.arange(_lat1, _lat2 + _dlat/2, _dlat)
|
|
197
|
+
|
|
198
|
+
elif line[60:].startswith('LON1 / LON2 / DLON'):
|
|
199
|
+
_lon1, _lon2, _dlon = [float(v) for v in line.split()[:3]]
|
|
200
|
+
longitudes_deg = np.arange(_lon1, _lon2 + _dlon/2, _dlon)
|
|
201
|
+
|
|
202
|
+
elif line[60:].startswith('EXPONENT'):
|
|
203
|
+
exponent: float = float(line[:6])
|
|
204
|
+
|
|
205
|
+
elif line[60:].startswith('END OF HEADER'):
|
|
206
|
+
i_body = i + 1
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
if header_mark_found is False:
|
|
210
|
+
raise ValueError(f'The input does not seem to be a IONEX file [ {doc[:10]} ]')
|
|
211
|
+
|
|
212
|
+
n_lines_lat_row = int(np.ceil(len(longitudes_deg) / 16))
|
|
213
|
+
|
|
214
|
+
current_gim = None
|
|
215
|
+
gim_type = None
|
|
216
|
+
|
|
217
|
+
# Body
|
|
218
|
+
for i in range(i_body, n_lines):
|
|
219
|
+
|
|
220
|
+
line = lines[i]
|
|
221
|
+
|
|
222
|
+
if line[60:].startswith('START OF TEC MAP'):
|
|
223
|
+
|
|
224
|
+
i_lat_row = 0
|
|
225
|
+
|
|
226
|
+
gim_type = gim.GimType.TEC
|
|
227
|
+
|
|
228
|
+
elif line[60:].startswith('START OF RMS MAP'):
|
|
229
|
+
|
|
230
|
+
i_lat_row = 0
|
|
231
|
+
|
|
232
|
+
gim_type = gim.GimType.RMS
|
|
233
|
+
|
|
234
|
+
elif line[60:].startswith('EPOCH OF CURRENT MAP'):
|
|
235
|
+
|
|
236
|
+
# Initialize map
|
|
237
|
+
current_gim = gim.Gim(_parse_ionex_epoch(line),
|
|
238
|
+
longitudes_deg, latitudes_deg,
|
|
239
|
+
[[0] * len(longitudes_deg)] * len(latitudes_deg))
|
|
240
|
+
|
|
241
|
+
elif line[60:].startswith('LAT/LON1/LON2/DLON/H'):
|
|
242
|
+
|
|
243
|
+
lat_row = ''.join([lines[i + 1 + j] for j in range(n_lines_lat_row)])
|
|
244
|
+
|
|
245
|
+
values = np.array([float(v) for v in lat_row.split()])
|
|
246
|
+
|
|
247
|
+
i += n_lines_lat_row
|
|
248
|
+
|
|
249
|
+
current_gim.vtec_values[i_lat_row] = (values * np.power(10, exponent)).tolist()
|
|
250
|
+
|
|
251
|
+
i_lat_row = i_lat_row + 1
|
|
252
|
+
|
|
253
|
+
# If end of map reached, send them to appropriate processor
|
|
254
|
+
elif line[60:].startswith('END OF TEC MAP') or line[60:].startswith('END OF RMS MAP'):
|
|
255
|
+
gim_handler.process(current_gim, gim_type)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _parse_ionex_epoch(ionex_line: str) -> datetime.datetime:
|
|
259
|
+
"""
|
|
260
|
+
Parse the epoch from a IONEX line
|
|
261
|
+
|
|
262
|
+
>>> _parse_ionex_epoch(" 2024 12 11 0 0 14 EPOCH OF FIRST MAP")
|
|
263
|
+
datetime.datetime(2024, 12, 11, 0, 0, 14)
|
|
264
|
+
>>> _parse_ionex_epoch(" 2024 12 11 0 0 0 EPOCH OF CURRENT MAP")
|
|
265
|
+
datetime.datetime(2024, 12, 11, 0, 0)
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
_HEADER_EPOCH_FORMAT = " %Y %m %d %H %M %S"
|
|
269
|
+
|
|
270
|
+
return datetime.datetime.strptime(ionex_line[:36], _HEADER_EPOCH_FORMAT)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class NeQuickGimHandlerArray(gim.GimHandler):
|
|
275
|
+
"""
|
|
276
|
+
Handler to store the incoming GIMs in arrays
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self):
|
|
280
|
+
self.vtec_gims: List[gim.Gim] = []
|
|
281
|
+
|
|
282
|
+
def process(self, nequick_gim: nequick.Gim):
|
|
283
|
+
"""
|
|
284
|
+
Process a GIM file
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
incoming_gim = gim.Gim(nequick_gim.epoch,
|
|
288
|
+
nequick_gim.longitudes, nequick_gim.latitudes,
|
|
289
|
+
nequick_gim.vtec_values)
|
|
290
|
+
|
|
291
|
+
self.vtec_gims.append(incoming_gim)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def cli():
|
|
295
|
+
"""
|
|
296
|
+
This function allows users to compute the difference between two IONEX files
|
|
297
|
+
or between an IONEX file and the NeQuick model (with three coefficients),
|
|
298
|
+
and save the result in a new IONEX file.
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
Compute the difference between two IONEX files:
|
|
303
|
+
$ python ionex.py file1.ionex file2.ionex output.ionex
|
|
304
|
+
|
|
305
|
+
Compute the difference between an IONEX file and the NeQuick model:
|
|
306
|
+
$ python ionex.py file1.ionex output.ionex --nequick 0.123 0.456 0.789
|
|
307
|
+
"""
|
|
308
|
+
parser = argparse.ArgumentParser(description=cli.__doc__,
|
|
309
|
+
formatter_class=argparse.RawDescriptionHelpFormatter )
|
|
310
|
+
|
|
311
|
+
parser.add_argument(
|
|
312
|
+
"lhs",
|
|
313
|
+
type=str,
|
|
314
|
+
help="Path to the first IONEX file (left-hand side).",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"output",
|
|
319
|
+
type=str,
|
|
320
|
+
help="Path to the output IONEX file where the differences will be saved.",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
rhs = parser.add_mutually_exclusive_group(required=True)
|
|
324
|
+
|
|
325
|
+
rhs.add_argument(
|
|
326
|
+
"--rhs",
|
|
327
|
+
type=str,
|
|
328
|
+
help="Path to the second IONEX file (right-hand side). If not provided, --nequick must be specified.",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
rhs.add_argument(
|
|
332
|
+
"--nequick",
|
|
333
|
+
type=float,
|
|
334
|
+
nargs=3,
|
|
335
|
+
metavar=("AZ0", "AZ1", "AZ2"),
|
|
336
|
+
help="Use the NeQuick model to compare against the 'lhs' IONEX (instead of another IONEX file). "
|
|
337
|
+
"Specify the three NeQuick coefficients (az0, az1, az2).",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
parser.add_argument(
|
|
341
|
+
"--nequick-ionex",
|
|
342
|
+
type=str,
|
|
343
|
+
default=None,
|
|
344
|
+
required=False,
|
|
345
|
+
metavar='<file>',
|
|
346
|
+
help="Specify a filename to store the NeQuick model in IONEX format",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
args = parser.parse_args()
|
|
350
|
+
|
|
351
|
+
PGM = "ionex_diff"
|
|
352
|
+
|
|
353
|
+
# Validate input arguments
|
|
354
|
+
if args.rhs is None and args.nequick is None:
|
|
355
|
+
parser.error("Either a second IONEX file (rhs) or the '--nequick' option must be provided.")
|
|
356
|
+
|
|
357
|
+
if args.rhs is not None and args.nequick is not None:
|
|
358
|
+
parser.error("You cannot specify both a second IONEX file (rhs) and the '--nequick' option.")
|
|
359
|
+
|
|
360
|
+
if args.nequick_ionex is not None and args.nequick is None:
|
|
361
|
+
parser.error("Cannot output the IONEX file with the NeQuick model without the '--nequick' option.")
|
|
362
|
+
|
|
363
|
+
# Process the lhs IONEX file
|
|
364
|
+
gim_handler_lhs = gim.GimHandlerArray()
|
|
365
|
+
load(args.lhs, gim_handler=gim_handler_lhs)
|
|
366
|
+
|
|
367
|
+
# Add comments to the output file
|
|
368
|
+
comment_lines = [
|
|
369
|
+
"This IONEX file contains the differences of VTEC values,",
|
|
370
|
+
"computed as vtec_left - vtec_right, where:",
|
|
371
|
+
f"- vtec_left: {os.path.basename(args.lhs)}"
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
# Process the rhs input (either an IONEX file or NeQuick coefficients)
|
|
375
|
+
gim_handler_rhs = None
|
|
376
|
+
if args.rhs:
|
|
377
|
+
gim_handler_rhs = gim.GimHandlerArray()
|
|
378
|
+
# Load the second IONEX file
|
|
379
|
+
load(args.rhs, gim_handler=gim_handler_rhs)
|
|
380
|
+
comment_lines += [f"- vtec_right: {os.path.basename(args.rhs)}"]
|
|
381
|
+
|
|
382
|
+
else:
|
|
383
|
+
gim_handler_rhs = NeQuickGimHandlerArray()
|
|
384
|
+
# Generate GIMs using NeQuick coefficients
|
|
385
|
+
coeffs = args.nequick
|
|
386
|
+
nequick_desc = ["NeQuick model", f" az0={coeffs[0]}", f" az1={coeffs[1]}", f" az2={coeffs[2]}"]
|
|
387
|
+
comment_lines += [f"- vtec_right: {nequick_desc[0]}"] + nequick_desc[1:]
|
|
388
|
+
|
|
389
|
+
for ionex_gim in gim_handler_lhs.vtec_gims:
|
|
390
|
+
|
|
391
|
+
nequick.to_gim(nequick.Coefficients(*coeffs),
|
|
392
|
+
ionex_gim.epoch,
|
|
393
|
+
latitudes = ionex_gim.latitudes,
|
|
394
|
+
longitudes = ionex_gim.longitudes,
|
|
395
|
+
gim_handler= gim_handler_rhs)
|
|
396
|
+
|
|
397
|
+
# Compute the difference
|
|
398
|
+
gim_diffs = gim.subtract_gims(gim_handler_lhs.vtec_gims, gim_handler_rhs.vtec_gims)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Write the result to the output file
|
|
402
|
+
write(args.output, gim_diffs, gim.GimType.TEC, pgm=PGM, comment_lines=comment_lines)
|
|
403
|
+
|
|
404
|
+
if args.nequick_ionex is not None:
|
|
405
|
+
comment_lines = [
|
|
406
|
+
"TEC values generated with the " + nequick_desc[0]
|
|
407
|
+
] + nequick_desc[1:]
|
|
408
|
+
|
|
409
|
+
write(args.nequick_ionex, gim_handler_lhs.vtec_gims, gim.GimType.TEC, pgm=PGM,
|
|
410
|
+
comment_lines=comment_lines )
|
pygnss/iono/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compute_pierce_point(receiver_ecef, sat_ecef, iono_height_m=350000.0):
|
|
5
|
+
"""
|
|
6
|
+
Compute the ionospheric pierce point given receiver and satellite positions.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
receiver_ecef : array-like
|
|
10
|
+
ECEF coordinates of the receiver (x, y, z) in meters.
|
|
11
|
+
sat_ecef : array-like
|
|
12
|
+
ECEF coordinates of the satellite (x, y, z) in meters.
|
|
13
|
+
iono_height_m : float
|
|
14
|
+
Height of the ionospheric shell in meters.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
pierce_point_ecef : array-like
|
|
18
|
+
ECEF coordinates of the pierce point (x, y, z) in meters.
|
|
19
|
+
|
|
20
|
+
>>> receiver_ecef = [0, 0, 6470000.0]
|
|
21
|
+
>>> sat_ecef = [0.0, 0.0, 20000000.0]
|
|
22
|
+
>>> pierce_point = compute_pierce_point(receiver_ecef, sat_ecef)
|
|
23
|
+
>>> np.round(pierce_point, 3)
|
|
24
|
+
array([ 0. , 0. , 6829466.77])
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
receiver_ecef = np.array(receiver_ecef)
|
|
28
|
+
sat_ecef = np.array(sat_ecef)
|
|
29
|
+
|
|
30
|
+
# Vector from receiver to satellite
|
|
31
|
+
rho = sat_ecef - receiver_ecef
|
|
32
|
+
rho_norm = np.linalg.norm(rho)
|
|
33
|
+
|
|
34
|
+
# Unit vector in the direction from receiver to satellite
|
|
35
|
+
rho_unit = rho / rho_norm
|
|
36
|
+
|
|
37
|
+
# Compute the distance from the Earth's center to the pierce point
|
|
38
|
+
r_receiver = np.linalg.norm(receiver_ecef)
|
|
39
|
+
r_iono = r_receiver + iono_height_m
|
|
40
|
+
|
|
41
|
+
# Compute the distance along the line of sight to the pierce point
|
|
42
|
+
d = (r_iono**2 - r_receiver**2) / (2.0 * np.dot(receiver_ecef, rho_unit))
|
|
43
|
+
|
|
44
|
+
# Compute the pierce point coordinates
|
|
45
|
+
pierce_point_ecef = receiver_ecef + d * rho_unit
|
|
46
|
+
|
|
47
|
+
return pierce_point_ecef
|
pygnss/iono/chapman.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to compute the Chapman ionospheric model.
|
|
3
|
+
|
|
4
|
+
This module provides functions to compute the Chapman ionospheric model,
|
|
5
|
+
which is used to estimate the electron density in the ionosphere based on
|
|
6
|
+
solar zenith angle and other parameters.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
HEIGHTS_KM = np.arange(60, 1000, 10)
|
|
12
|
+
|
|
13
|
+
def compute_profile(hmF2_km: float, NmF2: float, H_km: float, zenith_angle_deg: float, heights_km: float | np.ndarray =HEIGHTS_KM) -> float | np.ndarray:
|
|
14
|
+
"""
|
|
15
|
+
Compute the Chapman ionospheric model.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
zenith_angle (float or np.ndarray): Solar zenith angle, the angle between the vertical and the direction to the Sun.
|
|
19
|
+
hmF2_km (float): Height of the F2 layer in kilometers.
|
|
20
|
+
NmF2 (float): Peak electron density at hmF2 in electrons/m^3.
|
|
21
|
+
H_km (float): Scale height in kilometers (typical values between 30 and 50 km).
|
|
22
|
+
heights_km (np.ndarray, optional): Heights at which to compute the electron density.
|
|
23
|
+
If None, defaults to a range from 0 to 100 km.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
float or np.ndarray: Computed electron density based on the Chapman model.
|
|
27
|
+
"""
|
|
28
|
+
# Convert zenith angle from degrees to radians
|
|
29
|
+
zenith_angle_rad = np.radians(zenith_angle_deg)
|
|
30
|
+
|
|
31
|
+
# Include scale height
|
|
32
|
+
z = (heights_km - hmF2_km) / H_km
|
|
33
|
+
|
|
34
|
+
# Compute the Chapman function
|
|
35
|
+
return NmF2 * np.exp( 0.5 * (1 - z - np.exp(-z) / np.cos(zenith_angle_rad)))
|
pygnss/iono/gim.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import datetime
|
|
4
|
+
import enum
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GimType(enum.Enum):
|
|
11
|
+
"""
|
|
12
|
+
Type of Global Ionospheric Map (VTEC, RMS)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
TEC = enum.auto()
|
|
16
|
+
RMS = enum.auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Gim():
|
|
21
|
+
epoch: datetime.datetime
|
|
22
|
+
longitudes: List[float]
|
|
23
|
+
latitudes: List[float]
|
|
24
|
+
vtec_values: List[List[float]] # Grid of VTEC values n_latitudes (rows) x n_longitudes (columns)
|
|
25
|
+
|
|
26
|
+
def __sub__(self, other: 'Gim') -> 'Gim':
|
|
27
|
+
"""
|
|
28
|
+
Subtract the VTEC values of another Gim from this Gim
|
|
29
|
+
|
|
30
|
+
:param other: The Gim to subtract.
|
|
31
|
+
|
|
32
|
+
:return: A new Gim with the resulting VTEC values.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
return subtract(self, other)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GimHandler(ABC):
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def process(self, gim: Gim, type: GimType):
|
|
42
|
+
"""
|
|
43
|
+
Process a GIM file
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class GimHandlerArray(GimHandler):
|
|
48
|
+
"""
|
|
49
|
+
Handler to store the incoming GIMs in arrays
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.vtec_gims: List[Gim] = []
|
|
54
|
+
self.rms_gims: List[Gim] = []
|
|
55
|
+
|
|
56
|
+
def process(self, gim: Gim, type: GimType):
|
|
57
|
+
"""
|
|
58
|
+
Process a GIM file
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
if type == GimType.TEC:
|
|
62
|
+
self.vtec_gims.append(gim)
|
|
63
|
+
|
|
64
|
+
elif type == GimType.RMS:
|
|
65
|
+
self.rms_gims.append(gim)
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f'Gim Type [ {type} ] not supported')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def subtract(lhs: Gim, rhs: Gim) -> Gim:
|
|
72
|
+
"""
|
|
73
|
+
Subtract the VTEC values of two GIMs (lhs - rhs)
|
|
74
|
+
|
|
75
|
+
:param lhs: Left-hand operand
|
|
76
|
+
:param rhs: Right-hand operand
|
|
77
|
+
|
|
78
|
+
:return: A new Gim with the resulting difference of VTEC values.
|
|
79
|
+
|
|
80
|
+
:raises ValueError: If the dimensions of the GIMs do not match.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if lhs.epoch != rhs.epoch:
|
|
84
|
+
raise ValueError(f"Epochs of both GIMs differ: {lhs.epoch} != {rhs.epoch}")
|
|
85
|
+
|
|
86
|
+
if np.array_equal(lhs.latitudes, rhs.latitudes) == False:
|
|
87
|
+
raise ValueError("Latitudes do not match between the two GIMs.")
|
|
88
|
+
|
|
89
|
+
if np.array_equal(lhs.longitudes, rhs.longitudes) == False:
|
|
90
|
+
raise ValueError("Longitude do not match between the two GIMs.")
|
|
91
|
+
|
|
92
|
+
vtec_diff = np.subtract(lhs.vtec_values, rhs.vtec_values)
|
|
93
|
+
|
|
94
|
+
return Gim(
|
|
95
|
+
epoch=lhs.epoch, # Keep the epoch of the first Gim
|
|
96
|
+
longitudes=lhs.longitudes,
|
|
97
|
+
latitudes=lhs.latitudes,
|
|
98
|
+
vtec_values=vtec_diff.tolist(),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def subtract_gims(lhs: List[Gim], rhs: List[Gim]) -> List[Gim]:
|
|
103
|
+
"""
|
|
104
|
+
Subtract the VTEC values of two lists of GIMs (lhs - rhs).
|
|
105
|
+
|
|
106
|
+
The subtraction is performed only for GIMs with matching epochs, latitudes, and longitudes.
|
|
107
|
+
If a GIM in one list does not have a matching epoch in the other list, it is ignored.
|
|
108
|
+
|
|
109
|
+
:param lhs: The first list of GIMs (left-hand operand).
|
|
110
|
+
:param rhs: The second list of GIMs (right-hand operand).
|
|
111
|
+
:return: A list of GIMs resulting from the subtraction.
|
|
112
|
+
:raises ValueError: If latitudes or longitudes do not match for matching epochs.
|
|
113
|
+
"""
|
|
114
|
+
result = []
|
|
115
|
+
|
|
116
|
+
# Create a dictionary for quick lookup of GIMs in the rhs list by epoch
|
|
117
|
+
rhs_dict = {gim.epoch: gim for gim in rhs}
|
|
118
|
+
|
|
119
|
+
for gim_lhs in lhs:
|
|
120
|
+
|
|
121
|
+
# Check if there is a matching epoch in the rhs list
|
|
122
|
+
if gim_lhs.epoch in rhs_dict:
|
|
123
|
+
gim_rhs = rhs_dict[gim_lhs.epoch]
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result.append(gim_lhs - gim_rhs)
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
raise ValueError(f"Error subtracting GIMs for epoch {gim_lhs.epoch}: {e}")
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
pygnss/logger.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# TODO Move logging configuration to log.ini
|
|
6
|
+
# TODO Use slack handler for notifying error to slack rokubun group
|
|
7
|
+
|
|
8
|
+
FORMAT = '%(asctime)s - %(levelname)-8s - %(message)s'
|
|
9
|
+
EPOCH_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
logger.setLevel(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
13
|
+
console_handler = logging.StreamHandler()
|
|
14
|
+
formatter = logging.Formatter(FORMAT)
|
|
15
|
+
console_handler.setFormatter(formatter)
|
|
16
|
+
logger.addHandler(console_handler)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LevelLogFilter(object):
|
|
20
|
+
def __init__(self, levels):
|
|
21
|
+
self.__levels = levels
|
|
22
|
+
|
|
23
|
+
def filter(self, record):
|
|
24
|
+
return record.levelno in self.__levels
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def debug(message):
|
|
28
|
+
logger.debug(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def info(message):
|
|
32
|
+
logger.info(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def warning(message):
|
|
36
|
+
logger.warning(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def error(message):
|
|
40
|
+
logger.error(message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def critical(message, exception=None):
|
|
44
|
+
logger.critical(message, exc_info=exception)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def exception(message, exception):
|
|
48
|
+
logger.critical(message, exc_info=exception)
|
|
49
|
+
raise exception
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def log(level, message):
|
|
53
|
+
logger.log(logging._nameToLevel[level], message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_level(level):
|
|
57
|
+
logger.setLevel(level=level)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setFileHandler(filename):
|
|
61
|
+
handler = logging.FileHandler(filename)
|
|
62
|
+
handler.setLevel(logging.DEBUG)
|
|
63
|
+
handler.setFormatter(logging.Formatter(FORMAT))
|
|
64
|
+
logger.addHandler(handler)
|
|
65
|
+
return handler
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unsetHandler(handler):
|
|
69
|
+
handler.close()
|
|
70
|
+
logger.removeHandler(handler)
|