pygnss 0.4.0__cp313-cp313-musllinux_1_2_i686.whl → 0.6.0__cp313-cp313-musllinux_1_2_i686.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.
Potentially problematic release.
This version of pygnss might be problematic. Click here for more details.
- pygnss/__init__.py +1 -1
- pygnss/decorator.py +33 -0
- pygnss/ionex.py +302 -0
- pygnss/iono/gim.py +77 -0
- pygnss/nequick.py +13 -89
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/METADATA +1 -1
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/RECORD +11 -10
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/entry_points.txt +1 -0
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/WHEEL +0 -0
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pygnss-0.4.0.dist-info → pygnss-0.6.0.dist-info}/top_level.txt +0 -0
pygnss/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.6.0"
|
pygnss/decorator.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import subprocess
|
|
1
4
|
import warnings
|
|
2
5
|
|
|
3
6
|
|
|
@@ -12,3 +15,33 @@ def deprecated(alternative):
|
|
|
12
15
|
return func(*args, **kwargs)
|
|
13
16
|
return new_func
|
|
14
17
|
return decorator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_contents(func):
|
|
21
|
+
"""
|
|
22
|
+
Decorator to handle gzip compression based on filename and pass its contents
|
|
23
|
+
to the function
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrapper(filename, *args, **kwargs):
|
|
28
|
+
|
|
29
|
+
doc = None
|
|
30
|
+
|
|
31
|
+
if filename.endswith('.gz'):
|
|
32
|
+
with gzip.open(filename, 'rt', encoding='utf-8') as fh:
|
|
33
|
+
doc = fh.read()
|
|
34
|
+
elif filename.endswith('.Z'):
|
|
35
|
+
result = subprocess.run(['uncompress', '-c', filename],
|
|
36
|
+
stdout=subprocess.PIPE,
|
|
37
|
+
stderr=subprocess.PIPE,
|
|
38
|
+
check=True,
|
|
39
|
+
text=True)
|
|
40
|
+
doc = result.stdout
|
|
41
|
+
else:
|
|
42
|
+
with open(filename, 'rt', encoding='utf-8') as fh:
|
|
43
|
+
doc = fh.read()
|
|
44
|
+
|
|
45
|
+
return func(doc, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
return wrapper
|
pygnss/ionex.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .iono import gim
|
|
10
|
+
from .decorator import read_contents
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load(filename: str, gim_handler: gim.GimHandler):
|
|
14
|
+
"""
|
|
15
|
+
Load an IONEX file and process its contents using the provided GIM handler.
|
|
16
|
+
|
|
17
|
+
:param filename: The path to the IONEX file to load.
|
|
18
|
+
:param gim_handler: An instance of a GIM handler to process the GIMs read from the file.
|
|
19
|
+
:return: The result of processing the IONEX file.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
return _load(filename, gim_handler)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write(filename: str, gims: List[gim.Gim], gim_type: gim.GimType,
|
|
26
|
+
pgm: str = "pygnss", runby: str = "pygnss", comment_lines: List[str] = []) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Write a list of GIMs to an IONEX file.
|
|
29
|
+
|
|
30
|
+
:param filename: The path to the IONEX file to write.
|
|
31
|
+
:param gims: A list of GIM objects to write to the file.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
EXPONENT = -1
|
|
35
|
+
FACTOR = math.pow(10, EXPONENT)
|
|
36
|
+
|
|
37
|
+
if not gims:
|
|
38
|
+
raise ValueError("The list of GIMs is empty. Cannot write to the IONEX file.")
|
|
39
|
+
|
|
40
|
+
# Extract latitudes and longitudes from the first GIM
|
|
41
|
+
latitudes = gims[0].latitudes
|
|
42
|
+
longitudes = gims[0].longitudes
|
|
43
|
+
|
|
44
|
+
lon1 = longitudes[0]
|
|
45
|
+
lon2 = longitudes[-1]
|
|
46
|
+
dlon = longitudes[1] - longitudes[0]
|
|
47
|
+
|
|
48
|
+
# Ensure all GIMs have the same latitudes and longitudes
|
|
49
|
+
for gim_obj in gims:
|
|
50
|
+
if np.array_equal(gim_obj.latitudes, latitudes) == False or \
|
|
51
|
+
np.array_equal(gim_obj.longitudes, longitudes) == False:
|
|
52
|
+
raise ValueError("All GIMs must have the same latitudes and longitudes.")
|
|
53
|
+
|
|
54
|
+
# Sort the IONEX files by epoch
|
|
55
|
+
gims.sort(key=lambda gim: gim.epoch)
|
|
56
|
+
|
|
57
|
+
first_epoch = gims[0].epoch
|
|
58
|
+
last_epoch = gims[-1].epoch
|
|
59
|
+
n_maps = len(gims)
|
|
60
|
+
|
|
61
|
+
lat_0 = gims[0].latitudes[0]
|
|
62
|
+
lat_1 = gims[0].latitudes[-1]
|
|
63
|
+
dlat = gims[0].latitudes[1] - gims[0].latitudes[0]
|
|
64
|
+
|
|
65
|
+
# We will print the map from North to South, therefore check if the
|
|
66
|
+
# latitudes need to be reversed
|
|
67
|
+
latitude_reversal = lat_0 < lat_1
|
|
68
|
+
if latitude_reversal:
|
|
69
|
+
lat_0 = gims[0].latitudes[-1]
|
|
70
|
+
lat_1 = gims[0].latitudes[0]
|
|
71
|
+
dlat = gims[0].latitudes[0] - gims[0].latitudes[1]
|
|
72
|
+
|
|
73
|
+
lon_0 = gims[0].longitudes[0]
|
|
74
|
+
lon_1 = gims[0].longitudes[-1]
|
|
75
|
+
dlon = gims[0].longitudes[1] - gims[0].longitudes[0]
|
|
76
|
+
|
|
77
|
+
doc = ""
|
|
78
|
+
|
|
79
|
+
# Header
|
|
80
|
+
today = datetime.datetime.now()
|
|
81
|
+
epoch_str = today.strftime('%d-%b-%y %H:%M')
|
|
82
|
+
|
|
83
|
+
doc +=" 1.0 IONOSPHERE MAPS NEQUICK IONEX VERSION / TYPE\n"
|
|
84
|
+
doc +=f"{pgm[:20]:<20}{runby[:20]:<20}{epoch_str[:20]:<20}PGM / RUN BY / DATE\n"
|
|
85
|
+
|
|
86
|
+
for comment_line in comment_lines:
|
|
87
|
+
doc += f"{comment_line[:60]:<60}COMMENT\n"
|
|
88
|
+
|
|
89
|
+
doc += first_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF FIRST MAP\n")
|
|
90
|
+
doc += last_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF LAST MAP\n")
|
|
91
|
+
doc += " 0 INTERVAL\n"
|
|
92
|
+
doc += f"{n_maps:>6} # OF MAPS IN FILE\n"
|
|
93
|
+
doc += " NONE MAPPING FUNCTION\n"
|
|
94
|
+
doc += " 0.0 ELEVATION CUTOFF\n"
|
|
95
|
+
doc += " OBSERVABLES USED\n"
|
|
96
|
+
doc += " 6371.0 BASE RADIUS\n"
|
|
97
|
+
doc += " 2 MAP DIMENSION\n"
|
|
98
|
+
doc += " 450.0 450.0 0.0 HGT1 / HGT2 / DHGT\n"
|
|
99
|
+
doc += f" {lat_0:6.1f}{lat_1:6.1f}{dlat:6.1f} LAT1 / LAT2 / DLAT\n"
|
|
100
|
+
doc += f" {lon_0:6.1f}{lon_1:6.1f}{dlon:6.1f} LON1 / LON2 / DLON\n"
|
|
101
|
+
doc += f"{EXPONENT:>6} EXPONENT\n"
|
|
102
|
+
doc += " END OF HEADER\n"
|
|
103
|
+
|
|
104
|
+
# Write each GIM
|
|
105
|
+
for i_map, gim_obj in enumerate(gims):
|
|
106
|
+
|
|
107
|
+
doc += f"{i_map+1:>6} START OF {gim_type.name} MAP\n"
|
|
108
|
+
|
|
109
|
+
# Write the epoch
|
|
110
|
+
epoch = gim_obj.epoch
|
|
111
|
+
doc += epoch.strftime(" %Y %m %d %H %M %S EPOCH OF CURRENT MAP\n")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
for i, _ in enumerate(latitudes):
|
|
115
|
+
|
|
116
|
+
if latitude_reversal:
|
|
117
|
+
i = len(latitudes) - 1 - i
|
|
118
|
+
|
|
119
|
+
lat = latitudes[i]
|
|
120
|
+
|
|
121
|
+
doc += f" {lat:6.1f}{lon1:6.1f}{lon2:6.1f}{dlon:6.1f} 450.0 LAT/LON1/LON2/DLON/H\n"
|
|
122
|
+
|
|
123
|
+
lat_row = gim_obj.vtec_values[i]
|
|
124
|
+
for j in range(0, len(longitudes), 16):
|
|
125
|
+
doc += "".join(f"{round(vtec / FACTOR):5d}" for vtec in lat_row[j:j+16]) + "\n"
|
|
126
|
+
|
|
127
|
+
doc += f"{i_map+1:>6} END OF {gim_type.name} MAP\n"
|
|
128
|
+
|
|
129
|
+
# Tail
|
|
130
|
+
doc += " END OF FILE\n"
|
|
131
|
+
|
|
132
|
+
with open(filename, "wt") as fh:
|
|
133
|
+
fh.write(doc)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def diff(filename_lhs: str, filename_rhs: str, output_file: str, pgm="pygnss.ionex") -> None:
|
|
138
|
+
"""
|
|
139
|
+
Compute the difference between two IONEX files and write the result in IONEX format
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
gim_handler_lhs = gim.GimHandlerArray()
|
|
143
|
+
gim_handler_rhs = gim.GimHandlerArray()
|
|
144
|
+
|
|
145
|
+
load(filename_lhs, gim_handler=gim_handler_lhs)
|
|
146
|
+
load(filename_rhs, gim_handler=gim_handler_rhs)
|
|
147
|
+
|
|
148
|
+
gim_diffs = gim.subtract_gims(gim_handler_lhs.vtec_gims, gim_handler_rhs.vtec_gims)
|
|
149
|
+
|
|
150
|
+
comment_lines = [
|
|
151
|
+
"This IONEX file contains the differences of VTEC values,",
|
|
152
|
+
"computed as vtec_left - vtec_right, where:",
|
|
153
|
+
f"- vtec_left: {os.path.basename(filename_lhs)}",
|
|
154
|
+
f"- vtec_right: {os.path.basename(filename_rhs)}",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
write(output_file, gim_diffs, gim.GimType.TEC, pgm=pgm, comment_lines=comment_lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@read_contents
|
|
161
|
+
def _load(doc: str, gim_handler: gim.GimHandler):
|
|
162
|
+
"""
|
|
163
|
+
Parse the contents of an IONEX file and process each GIM using the provided handler.
|
|
164
|
+
|
|
165
|
+
:param doc: The contents of the IONEX file as a string.
|
|
166
|
+
:param gim_handler: An instance of a GIM handler to process the GIMs read from the file.
|
|
167
|
+
:raises ValueError: If the file is not a valid IONEX file or contains unsupported features.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
lines = doc.splitlines()
|
|
171
|
+
n_lines = len(lines)
|
|
172
|
+
i_body = 0
|
|
173
|
+
|
|
174
|
+
latitudes_deg: List[float] = []
|
|
175
|
+
longitudes_deg: List[float] = []
|
|
176
|
+
|
|
177
|
+
header_mark_found = False
|
|
178
|
+
|
|
179
|
+
# Header
|
|
180
|
+
for i in range(n_lines):
|
|
181
|
+
|
|
182
|
+
line = lines[i]
|
|
183
|
+
|
|
184
|
+
if line[60:].startswith('IONEX VERSION / TYPE'):
|
|
185
|
+
header_mark_found = True
|
|
186
|
+
|
|
187
|
+
elif line[60:].startswith('HGT1 / HGT2 / DHGT'):
|
|
188
|
+
_hgt1, _hgt2, _dhgt = [float(v) for v in line.split()[:3]]
|
|
189
|
+
if _dhgt != 0.0:
|
|
190
|
+
raise ValueError('Multi-layer Ionex files not supported')
|
|
191
|
+
|
|
192
|
+
elif line[60:].startswith('LAT1 / LAT2 / DLAT'):
|
|
193
|
+
_lat1, _lat2, _dlat = [float(v) for v in line.split()[:3]]
|
|
194
|
+
latitudes_deg = np.arange(_lat1, _lat2 + _dlat/2, _dlat)
|
|
195
|
+
|
|
196
|
+
elif line[60:].startswith('LON1 / LON2 / DLON'):
|
|
197
|
+
_lon1, _lon2, _dlon = [float(v) for v in line.split()[:3]]
|
|
198
|
+
longitudes_deg = np.arange(_lon1, _lon2 + _dlon/2, _dlon)
|
|
199
|
+
|
|
200
|
+
elif line[60:].startswith('EXPONENT'):
|
|
201
|
+
exponent: float = float(line[:6])
|
|
202
|
+
|
|
203
|
+
elif line[60:].startswith('END OF HEADER'):
|
|
204
|
+
i_body = i + 1
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
if header_mark_found is False:
|
|
208
|
+
raise ValueError(f'The input does not seem to be a IONEX file [ {doc[:10]} ]')
|
|
209
|
+
|
|
210
|
+
n_lines_lat_row = int(np.ceil(len(longitudes_deg) / 16))
|
|
211
|
+
|
|
212
|
+
current_gim = None
|
|
213
|
+
gim_type = None
|
|
214
|
+
|
|
215
|
+
# Body
|
|
216
|
+
for i in range(i_body, n_lines):
|
|
217
|
+
|
|
218
|
+
line = lines[i]
|
|
219
|
+
|
|
220
|
+
if line[60:].startswith('START OF TEC MAP'):
|
|
221
|
+
|
|
222
|
+
i_lat_row = 0
|
|
223
|
+
|
|
224
|
+
gim_type = gim.GimType.TEC
|
|
225
|
+
|
|
226
|
+
elif line[60:].startswith('START OF RMS MAP'):
|
|
227
|
+
|
|
228
|
+
i_lat_row = 0
|
|
229
|
+
|
|
230
|
+
gim_type = gim.GimType.RMS
|
|
231
|
+
|
|
232
|
+
elif line[60:].startswith('EPOCH OF CURRENT MAP'):
|
|
233
|
+
|
|
234
|
+
# Initialize map
|
|
235
|
+
current_gim = gim.Gim(_parse_ionex_epoch(line),
|
|
236
|
+
longitudes_deg, latitudes_deg,
|
|
237
|
+
[[0] * len(longitudes_deg)] * len(latitudes_deg))
|
|
238
|
+
|
|
239
|
+
elif line[60:].startswith('LAT/LON1/LON2/DLON/H'):
|
|
240
|
+
|
|
241
|
+
lat_row = ''.join([lines[i + 1 + j] for j in range(n_lines_lat_row)])
|
|
242
|
+
|
|
243
|
+
values = np.array([float(v) for v in lat_row.split()])
|
|
244
|
+
|
|
245
|
+
i += n_lines_lat_row
|
|
246
|
+
|
|
247
|
+
current_gim.vtec_values[i_lat_row] = (values * np.power(10, exponent)).tolist()
|
|
248
|
+
|
|
249
|
+
i_lat_row = i_lat_row + 1
|
|
250
|
+
|
|
251
|
+
# If end of map reached, send them to appropriate processor
|
|
252
|
+
elif line[60:].startswith('END OF TEC MAP') or line[60:].startswith('END OF RMS MAP'):
|
|
253
|
+
gim_handler.process(current_gim, gim_type)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _parse_ionex_epoch(ionex_line: str) -> datetime.datetime:
|
|
257
|
+
"""
|
|
258
|
+
Parse the epoch from a IONEX line
|
|
259
|
+
|
|
260
|
+
>>> _parse_ionex_epoch(" 2024 12 11 0 0 14 EPOCH OF FIRST MAP")
|
|
261
|
+
datetime.datetime(2024, 12, 11, 0, 0, 14)
|
|
262
|
+
>>> _parse_ionex_epoch(" 2024 12 11 0 0 0 EPOCH OF CURRENT MAP")
|
|
263
|
+
datetime.datetime(2024, 12, 11, 0, 0)
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
_HEADER_EPOCH_FORMAT = " %Y %m %d %H %M %S"
|
|
267
|
+
|
|
268
|
+
return datetime.datetime.strptime(ionex_line[:36], _HEADER_EPOCH_FORMAT)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def cli():
|
|
272
|
+
"""
|
|
273
|
+
This function allows users to compute the difference between two IONEX files
|
|
274
|
+
and save the result in a new IONEX file.
|
|
275
|
+
"""
|
|
276
|
+
parser = argparse.ArgumentParser(
|
|
277
|
+
description="Compute the difference between two IONEX files and save the result in a new IONEX file."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"lhs",
|
|
282
|
+
type=str,
|
|
283
|
+
help="Path to the first IONEX file (left-hand side).",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
parser.add_argument(
|
|
287
|
+
"rhs",
|
|
288
|
+
type=str,
|
|
289
|
+
help="Path to the second IONEX file (right-hand side).",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
parser.add_argument(
|
|
293
|
+
"output",
|
|
294
|
+
type=str,
|
|
295
|
+
help="Path to the output IONEX file where the differences will be saved.",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
|
|
300
|
+
PGM = "ionex_diff"
|
|
301
|
+
|
|
302
|
+
diff(args.lhs, args.rhs, args.output, pgm=PGM)
|
pygnss/iono/gim.py
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
import datetime
|
|
4
|
+
import enum
|
|
3
5
|
from typing import List
|
|
4
6
|
|
|
5
7
|
import numpy as np
|
|
6
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
|
+
|
|
7
19
|
@dataclass
|
|
8
20
|
class Gim():
|
|
9
21
|
epoch: datetime.datetime
|
|
@@ -23,6 +35,39 @@ class Gim():
|
|
|
23
35
|
return subtract(self, other)
|
|
24
36
|
|
|
25
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 = []
|
|
54
|
+
self.rms_gims = []
|
|
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
|
+
|
|
26
71
|
def subtract(lhs: Gim, rhs: Gim) -> Gim:
|
|
27
72
|
"""
|
|
28
73
|
Subtract the VTEC values of two GIMs (lhs - rhs)
|
|
@@ -52,3 +97,35 @@ def subtract(lhs: Gim, rhs: Gim) -> Gim:
|
|
|
52
97
|
latitudes=lhs.latitudes,
|
|
53
98
|
vtec_values=vtec_diff.tolist(),
|
|
54
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/nequick.py
CHANGED
|
@@ -6,6 +6,9 @@ import numpy as np
|
|
|
6
6
|
|
|
7
7
|
import nequick
|
|
8
8
|
|
|
9
|
+
from pygnss import ionex
|
|
10
|
+
import pygnss.iono.gim
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class GimIonexHandler(nequick.GimHandler):
|
|
11
14
|
"""
|
|
@@ -32,102 +35,23 @@ class GimIonexHandler(nequick.GimHandler):
|
|
|
32
35
|
|
|
33
36
|
self._gims.append(gim)
|
|
34
37
|
|
|
35
|
-
def to_ionex(self, pgm: str = "pygnss", runby: str = "pygnss") ->
|
|
36
|
-
|
|
37
|
-
EXPONENT = -1
|
|
38
|
-
|
|
39
|
-
# Sort the IONEX files by epoch
|
|
40
|
-
self._gims.sort(key=lambda gim: gim.epoch)
|
|
41
|
-
|
|
42
|
-
first_epoch = self._gims[0].epoch
|
|
43
|
-
last_epoch = self._gims[-1].epoch
|
|
44
|
-
n_maps = len(self._gims)
|
|
45
|
-
|
|
46
|
-
lat_0 = self._gims[0].latitudes[0]
|
|
47
|
-
lat_1 = self._gims[0].latitudes[-1]
|
|
48
|
-
dlat = self._gims[0].latitudes[1] - self._gims[0].latitudes[0]
|
|
49
|
-
|
|
50
|
-
# We will print the map from North to South, therefore check if the
|
|
51
|
-
# latitudes need to be reversed
|
|
52
|
-
latitude_reversal = lat_0 < lat_1
|
|
53
|
-
if latitude_reversal:
|
|
54
|
-
lat_0 = self._gims[0].latitudes[-1]
|
|
55
|
-
lat_1 = self._gims[0].latitudes[0]
|
|
56
|
-
dlat = self._gims[0].latitudes[0] - self._gims[0].latitudes[1]
|
|
57
|
-
|
|
58
|
-
lon_0 = self._gims[0].longitudes[0]
|
|
59
|
-
lon_1 = self._gims[0].longitudes[-1]
|
|
60
|
-
dlon = self._gims[0].longitudes[1] - self._gims[0].longitudes[0]
|
|
61
|
-
|
|
62
|
-
doc = ""
|
|
63
|
-
|
|
64
|
-
# Header
|
|
65
|
-
today = datetime.datetime.now()
|
|
66
|
-
epoch_str = today.strftime('%d-%b-%y %H:%M')
|
|
67
|
-
|
|
68
|
-
doc +=" 1.0 IONOSPHERE MAPS NEQUICK IONEX VERSION / TYPE\n"
|
|
69
|
-
doc +=f"{pgm[:20]:<20}{runby[:20]:<20}{epoch_str[:20]:<20}PGM / RUN BY / DATE\n"
|
|
70
|
-
doc +="Maps computed using the NeQuick model with the following COMMENT\n"
|
|
71
|
-
doc +="coefficients: COMMENT\n"
|
|
72
|
-
doc += f"{EXPONENT:>6} EXPONENT\n"
|
|
73
|
-
doc +=f"a0={self._coeffs.a0:<17.6f}a1={self._coeffs.a1:<17.8f}a2={self._coeffs.a2:<17.11f}COMMENT\n"
|
|
74
|
-
doc += first_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF FIRST MAP\n")
|
|
75
|
-
doc += last_epoch.strftime(" %Y %m %d %H %M %S EPOCH OF LAST MAP\n")
|
|
76
|
-
doc += " 0 INTERVAL\n"
|
|
77
|
-
doc += f"{n_maps:>6} # OF MAPS IN FILE\n"
|
|
78
|
-
doc += " NONE MAPPING FUNCTION\n"
|
|
79
|
-
doc += " 0.0 ELEVATION CUTOFF\n"
|
|
80
|
-
doc += " OBSERVABLES USED\n"
|
|
81
|
-
doc += " 6371.0 BASE RADIUS\n"
|
|
82
|
-
doc += " 2 MAP DIMENSION\n"
|
|
83
|
-
doc += " 450.0 450.0 0.0 HGT1 / HGT2 / DHGT\n"
|
|
84
|
-
doc += f" {lat_0:6.1f}{lat_1:6.1f}{dlat:6.1f} LAT1 / LAT2 / DLAT\n"
|
|
85
|
-
doc += f" {lon_0:6.1f}{lon_1:6.1f}{dlon:6.1f} LON1 / LON2 / DLON\n"
|
|
86
|
-
doc += " END OF HEADER\n"
|
|
87
|
-
|
|
88
|
-
# Body: For each GIM file, write the VTEC values
|
|
89
|
-
for i, gim in enumerate(self._gims):
|
|
38
|
+
def to_ionex(self, filename: str, pgm: str = "pygnss", runby: str = "pygnss") -> None:
|
|
90
39
|
|
|
91
|
-
|
|
92
|
-
|
|
40
|
+
comment_lines = [
|
|
41
|
+
"Maps computed using the NeQuick model with the following",
|
|
42
|
+
"coefficients:",
|
|
43
|
+
f"a0={self._coeffs.a0:<17.6f}a1={self._coeffs.a1:<17.8f}a2={self._coeffs.a2:<17.11f}"
|
|
44
|
+
]
|
|
93
45
|
|
|
94
|
-
|
|
46
|
+
ionex.write(filename, self._gims, pygnss.iono.gim.GimType.TEC, pgm, runby,
|
|
47
|
+
comment_lines=comment_lines)
|
|
95
48
|
|
|
96
|
-
for i_lat, lat in enumerate(gim.latitudes):
|
|
97
49
|
|
|
98
|
-
|
|
99
|
-
i_lat = n_latitudes - 1 - i_lat
|
|
100
|
-
|
|
101
|
-
lat = gim.latitudes[i_lat]
|
|
102
|
-
doc += f" {lat:6.1f}{lon_0:6.1f}{lon_1:6.1f}{dlon:6.1f} 450.0 LAT/LON1/LON2/DLON/H"
|
|
103
|
-
|
|
104
|
-
lat_row = gim.vtec_values[i_lat]
|
|
105
|
-
for i_lon, _ in enumerate(gim.longitudes):
|
|
106
|
-
|
|
107
|
-
if i_lon % 16 == 0:
|
|
108
|
-
doc += "\n"
|
|
109
|
-
|
|
110
|
-
vtec = lat_row[i_lon] / math.pow(10, EXPONENT)
|
|
111
|
-
doc += f"{int(vtec):>5d}"
|
|
112
|
-
|
|
113
|
-
doc += "\n"
|
|
114
|
-
|
|
115
|
-
doc += f"{i+1:>6} END OF TEC MAP\n"
|
|
116
|
-
|
|
117
|
-
# Tail
|
|
118
|
-
doc += " END OF FILE\n"
|
|
119
|
-
|
|
120
|
-
return doc
|
|
121
|
-
|
|
122
|
-
def to_ionex(coeffs: nequick.Coefficients, dates: List[datetime.datetime]) -> str:
|
|
123
|
-
|
|
124
|
-
doc = None
|
|
50
|
+
def to_ionex(filename: str, coeffs: nequick.Coefficients, dates: List[datetime.datetime]):
|
|
125
51
|
|
|
126
52
|
gim_handler = GimIonexHandler(coeffs)
|
|
127
53
|
|
|
128
54
|
for date in dates:
|
|
129
55
|
nequick.to_gim(coeffs, date, gim_handler=gim_handler)
|
|
130
56
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return doc
|
|
57
|
+
gim_handler.to_ionex(filename)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
pygnss/__init__.py,sha256=
|
|
1
|
+
pygnss/__init__.py,sha256=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
|
|
2
2
|
pygnss/_c_ext.cpython-313-i386-linux-musl.so,sha256=9CwtpxkeyS2ajcDuwlLpG1R9QsgwtBkl6wMFDeK4LNw,81152
|
|
3
3
|
pygnss/cl.py,sha256=ISmd2RjikUMmj3nLPN0VSjvQLG5rLizp2X2ajeBkoDE,4509
|
|
4
4
|
pygnss/constants.py,sha256=1hF6K92X6E6Ofo0rAuCBCgrwln9jxio26RV2a6vyURk,133
|
|
5
|
-
pygnss/decorator.py,sha256=
|
|
5
|
+
pygnss/decorator.py,sha256=qB-0jl2GTEHJdvmDruNll5X3RrdySssv94u9Hok5-lA,1438
|
|
6
6
|
pygnss/file.py,sha256=kkMBWjoTPkxJD1UgH0mXJT2fxnhU8u7_l2Ph5Xz2-hY,933
|
|
7
7
|
pygnss/geodetic.py,sha256=3q8Rpl4b5CxGlhdn1nQRBHHSW1v-0PBFz54zOeVyO74,33633
|
|
8
8
|
pygnss/hatanaka.py,sha256=P9XG6bZwUzfAPYn--6-DXfFQIEefeimE7fMJm_DF5zE,1951
|
|
9
|
+
pygnss/ionex.py,sha256=Sk0U_hRyYE7cEwLyMIgiY-2bBs6wktzKmZdI92HrU7c,10422
|
|
9
10
|
pygnss/logger.py,sha256=4kvcTWXPoiG-MlyP6B330l4Fu7MfCuDjuIlIiLA8f1Y,1479
|
|
10
|
-
pygnss/nequick.py,sha256=
|
|
11
|
+
pygnss/nequick.py,sha256=tSsUw3FoRWS-TBKKVb0_c90DFz_2jUTkUvNPsGQOLA8,1722
|
|
11
12
|
pygnss/rinex.py,sha256=LsOOh3Fc263kkM8KOUBNeMeIAmbOn2ASSBO4rAUJWj8,68783
|
|
12
13
|
pygnss/sinex.py,sha256=nErOmGCFFmGSnmWGNTJhaj3yZ6IIB8GgtW5WPypJc6U,3057
|
|
13
14
|
pygnss/stats.py,sha256=GYZfcyDvbM9xamWIyVlqyN5-DPJzTLJrybRrcNV6Z6o,1912
|
|
@@ -26,14 +27,14 @@ pygnss/gnss/observables.py,sha256=0x0NLkTjxf8cO9F_f_Q1b-1hEeoNjWB2x-53ecUEv0M,16
|
|
|
26
27
|
pygnss/gnss/residuals.py,sha256=8qKGNOYkrqxHGOSjIfH21K82PAqEh2068kf78j5usL8,1244
|
|
27
28
|
pygnss/gnss/types.py,sha256=lmL15KRckRiTwVkYvGzF4c1BrojnQlegrYCXSz1hGaI,10377
|
|
28
29
|
pygnss/iono/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
pygnss/iono/gim.py,sha256=
|
|
30
|
+
pygnss/iono/gim.py,sha256=bJ-1jRIrkZj790fuCk3np72NrPaG_Qvx-EVZsvWMfsk,3521
|
|
30
31
|
pygnss/orbit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
32
|
pygnss/orbit/kepler.py,sha256=QORTgg5yBtsQXxLWSzoZ1pmh-CwPiZlFdIYqhQhv1a0,1745
|
|
32
33
|
pygnss/orbit/tle.py,sha256=6CIEielPgui3DXNv46XxOGlig31ROIwjH42xLGaeE5M,5905
|
|
33
34
|
pygnss/parsers/rtklib/stats.py,sha256=YV6yadxMeQMQYZvsUCaSf4ZTpK8Bbv3f2xgu0l4PekA,5449
|
|
34
|
-
pygnss-0.
|
|
35
|
-
pygnss-0.
|
|
36
|
-
pygnss-0.
|
|
37
|
-
pygnss-0.
|
|
38
|
-
pygnss-0.
|
|
39
|
-
pygnss-0.
|
|
35
|
+
pygnss-0.6.0.dist-info/METADATA,sha256=0gGj855FG6F9WGYHCsZ3gbEPX8avn7JP_P5r3QNExa4,1659
|
|
36
|
+
pygnss-0.6.0.dist-info/WHEEL,sha256=qeaIuaJB-6C86fEu4NQSDId-fDkNNB1_xWNvSM-4W7I,110
|
|
37
|
+
pygnss-0.6.0.dist-info/entry_points.txt,sha256=awWUCMfX3uMIAPPZqZfSUuw7Kd7defip8L7GtthKqgg,292
|
|
38
|
+
pygnss-0.6.0.dist-info/top_level.txt,sha256=oZRSR-qOv98VW2PRRMGCVNCJmewcJjyJYmxzxfeimtg,7
|
|
39
|
+
pygnss-0.6.0.dist-info/RECORD,,
|
|
40
|
+
pygnss-0.6.0.dist-info/licenses/LICENSE,sha256=Wwany6RAAZ9vVHjFLA9KBJ0HE77d52s2NOUA1CPAEug,1067
|
|
File without changes
|
|
File without changes
|
|
File without changes
|