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/rinex.py
ADDED
|
@@ -0,0 +1,2161 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass, fields, asdict
|
|
4
|
+
import datetime
|
|
5
|
+
import enum
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import re
|
|
11
|
+
from typing import Dict, List, Tuple, Union, IO
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import logger
|
|
15
|
+
from . import time
|
|
16
|
+
from .constants import SPEED_OF_LIGHT
|
|
17
|
+
from .file import process_filename_or_file_handler, skip_lines
|
|
18
|
+
from .gnss.types import ConstellationId, Band, TrackingChannel, Satellite
|
|
19
|
+
|
|
20
|
+
from .orbit.tle import TLE, read_celestrak
|
|
21
|
+
from .orbit.kepler import Kepler
|
|
22
|
+
|
|
23
|
+
RINEX_LINE_SYS_OBS_TYPES = "SYS / # / OBS TYPES"
|
|
24
|
+
|
|
25
|
+
RANDOM_STR = 'random'
|
|
26
|
+
ZERO_STR = 'zero'
|
|
27
|
+
|
|
28
|
+
SAT_STR = 'sat'
|
|
29
|
+
EPOCH_STR = 'epoch'
|
|
30
|
+
A_M_STR = 'a_m'
|
|
31
|
+
ECCENTRICITY_STR = 'eccentricity'
|
|
32
|
+
INCLINATION_DEG_STR = 'inclination_deg'
|
|
33
|
+
RIGHT_ASCENSION_DEG_STR = 'raan_deg'
|
|
34
|
+
ARG_PERIGEE_DEG_STR = 'arg_perigee_deg'
|
|
35
|
+
TRUE_ANOMALY_DEG_STR = 'true_anomaly_deg'
|
|
36
|
+
|
|
37
|
+
RINEX_BAND_MAP = {
|
|
38
|
+
ConstellationId.GPS: {
|
|
39
|
+
'1': Band.L1,
|
|
40
|
+
'2': Band.L2,
|
|
41
|
+
'5': Band.L5
|
|
42
|
+
},
|
|
43
|
+
ConstellationId.GLONASS: {
|
|
44
|
+
'1': Band.G1,
|
|
45
|
+
'4': Band.G1a,
|
|
46
|
+
'2': Band.G2,
|
|
47
|
+
'6': Band.G2a,
|
|
48
|
+
'3': Band.G3
|
|
49
|
+
},
|
|
50
|
+
ConstellationId.GALILEO: {
|
|
51
|
+
'1': Band.E1,
|
|
52
|
+
'5': Band.E5a,
|
|
53
|
+
'7': Band.E5b,
|
|
54
|
+
'8': Band.E5,
|
|
55
|
+
'6': Band.E6
|
|
56
|
+
},
|
|
57
|
+
ConstellationId.BEIDOU: {
|
|
58
|
+
'2': Band.B1_2,
|
|
59
|
+
'1': Band.B1,
|
|
60
|
+
'5': Band.B2a,
|
|
61
|
+
'7': Band.B2b,
|
|
62
|
+
'8': Band.B2,
|
|
63
|
+
'6': Band.B3
|
|
64
|
+
},
|
|
65
|
+
ConstellationId.QZSS: {
|
|
66
|
+
'1': Band.L1,
|
|
67
|
+
'2': Band.L2,
|
|
68
|
+
'5': Band.L5,
|
|
69
|
+
'6': Band.L6
|
|
70
|
+
},
|
|
71
|
+
ConstellationId.IRNSS: {
|
|
72
|
+
'5': Band.L5,
|
|
73
|
+
'9': Band.S
|
|
74
|
+
},
|
|
75
|
+
ConstellationId.SBAS: {
|
|
76
|
+
'1': Band.L1,
|
|
77
|
+
'5': Band.L5
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class EphType(enum.Enum):
|
|
83
|
+
|
|
84
|
+
GPS_LNAV = f'{ConstellationId.GPS.value}_LNAV'
|
|
85
|
+
GPS_CNAV = f'{ConstellationId.GPS.value}_CNAV'
|
|
86
|
+
GPS_CNV2 = f'{ConstellationId.GPS.value}_CNV2'
|
|
87
|
+
GAL_INAV = f'{ConstellationId.GALILEO.value}_INAV'
|
|
88
|
+
GAL_FNAV = f'{ConstellationId.GALILEO.value}_FNAV'
|
|
89
|
+
GLO_FDMA = f'{ConstellationId.GLONASS.value}_FDMA'
|
|
90
|
+
QZS_LNAV = f'{ConstellationId.QZSS.value}_LNAV'
|
|
91
|
+
QZS_CNAV = f'{ConstellationId.QZSS.value}_CNAV'
|
|
92
|
+
QZS_CNV2 = f'{ConstellationId.QZSS.value}_CNV2'
|
|
93
|
+
BDS_D1 = f'{ConstellationId.BEIDOU.value}_D1'
|
|
94
|
+
BDS_D2 = f'{ConstellationId.BEIDOU.value}_D2'
|
|
95
|
+
BDS_CNV1 = f'{ConstellationId.BEIDOU.value}_CNV1'
|
|
96
|
+
BDS_CNV2 = f'{ConstellationId.BEIDOU.value}_CNV2'
|
|
97
|
+
BDS_CNV3 = f'{ConstellationId.BEIDOU.value}_CNV3'
|
|
98
|
+
SBS = f'{ConstellationId.SBAS.value}_SBAS'
|
|
99
|
+
IRN_LNAV = f'{ConstellationId.IRNSS.value}_LNAV'
|
|
100
|
+
LEO = f'{ConstellationId.LEO.value}'
|
|
101
|
+
SPIRE = f'{ConstellationId.SPIRE.value}'
|
|
102
|
+
STARLINK = f'{ConstellationId.STARLINK.value}'
|
|
103
|
+
ONEWEB = f'{ConstellationId.ONEWEB.value}'
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def from_string(value: str) -> 'EphType':
|
|
107
|
+
"""
|
|
108
|
+
Get the Ephemerides type from an input string (or raise an exception if not found)
|
|
109
|
+
|
|
110
|
+
>>> EphType.from_string('G_LNAV')
|
|
111
|
+
<EphType.GPS_LNAV: 'G_LNAV'>
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
for member in EphType:
|
|
115
|
+
if member.value == value:
|
|
116
|
+
return member
|
|
117
|
+
|
|
118
|
+
raise ValueError(f"Value [ {value} ] could not be mapped into a Rinex 4 ephemeris type")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class RinexSatIdProvider(ABC):
|
|
122
|
+
"""
|
|
123
|
+
This abstract class provides an interface to provide the RINEX Satellite ID and constellation
|
|
124
|
+
"""
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def get_constellation_letter(self) -> str:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def get_sat_number(self, norad_id) -> int:
|
|
130
|
+
return norad_id
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RinexSatIdFactory:
|
|
134
|
+
@staticmethod
|
|
135
|
+
def create(constellation: ConstellationId) -> RinexSatIdProvider:
|
|
136
|
+
if constellation == ConstellationId.STARLINK:
|
|
137
|
+
return StarlinkRNXId()
|
|
138
|
+
elif constellation == ConstellationId.SPIRE:
|
|
139
|
+
return SpireRNXId()
|
|
140
|
+
elif constellation == ConstellationId.ONEWEB:
|
|
141
|
+
return OneWebRNXId()
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError("Invalid constellation")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class StarlinkRNXId(RinexSatIdProvider):
|
|
147
|
+
def get_constellation_letter(self) -> str:
|
|
148
|
+
return ConstellationId.STARLINK.value
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SpireRNXId(RinexSatIdProvider):
|
|
152
|
+
def get_constellation_letter(self) -> str:
|
|
153
|
+
return ConstellationId.SPIRE.value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class OneWebRNXId(RinexSatIdProvider):
|
|
157
|
+
def get_constellation_letter(self) -> str:
|
|
158
|
+
return ConstellationId.ONEWEB.value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class ObservableType(object):
|
|
163
|
+
type: str
|
|
164
|
+
channel: TrackingChannel
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def from_string(observable_type: str) -> 'ObservableType':
|
|
168
|
+
|
|
169
|
+
if len(observable_type) != 3:
|
|
170
|
+
raise ValueError(f'Invalid length for observable type [ {observable_type} ] (must conform to RINEX v3 3-char format)')
|
|
171
|
+
return ObservableType(observable_type[0], TrackingChannel.from_rinex3_code(observable_type))
|
|
172
|
+
|
|
173
|
+
def __repr__(self):
|
|
174
|
+
return f'{self.type:1s}{self.channel}'
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class ObservableValue(object):
|
|
179
|
+
value: float
|
|
180
|
+
lli: int
|
|
181
|
+
snr: int
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Clock(object):
|
|
186
|
+
bias_s: float
|
|
187
|
+
drift_s_per_s: float
|
|
188
|
+
drift_rate_s_per_s2: float
|
|
189
|
+
|
|
190
|
+
ref_epoch: datetime = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ClockModel(ABC):
|
|
194
|
+
"""
|
|
195
|
+
This abstract class provides with an interface to provide with a Clock model
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def get_clock(self, satellite: Satellite, epoch: datetime.datetime) -> Clock:
|
|
200
|
+
"""
|
|
201
|
+
Get the clock for a satellite and an epoch
|
|
202
|
+
"""
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ZeroClockModel(ClockModel):
|
|
207
|
+
"""
|
|
208
|
+
Clock model with 0 bias and drift
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def get_clock(self, satellite: Satellite, epoch: datetime.datetime) -> Clock:
|
|
212
|
+
return Clock(0, 0, 0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class GnssRandomClock(ClockModel):
|
|
216
|
+
"""
|
|
217
|
+
Generate a clock model with random realizations for the clock bias and
|
|
218
|
+
drift.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, bias_max_s: float = 0.0005, drift_max_s_per_s: float = 1.0e-11):
|
|
222
|
+
"""
|
|
223
|
+
Initialize the object with the threshold values for the bias and drift.
|
|
224
|
+
In certain works, the typical values of bias and drift for GPS, Galileo
|
|
225
|
+
and Beidou are usually contained between -/+0.5ms and-/+1e-11 respectively
|
|
226
|
+
"""
|
|
227
|
+
self.bias_max_s = bias_max_s
|
|
228
|
+
self.drift_max_s_per_s = drift_max_s_per_s
|
|
229
|
+
|
|
230
|
+
# Internal memory to store the state of the previous clock
|
|
231
|
+
self.clocks = {}
|
|
232
|
+
|
|
233
|
+
def get_clock(self, satellite: Satellite, epoch: datetime.datetime) -> Clock:
|
|
234
|
+
|
|
235
|
+
clock = self.clocks.get(satellite, None)
|
|
236
|
+
|
|
237
|
+
if clock is None:
|
|
238
|
+
bias_s = np.random.uniform(low=-self.bias_max_s, high=self.bias_max_s)
|
|
239
|
+
drift_s_per_s = np.random.uniform(low=-self.drift_max_s_per_s, high=self.drift_max_s_per_s)
|
|
240
|
+
|
|
241
|
+
clock = Clock(bias_s, drift_s_per_s, 0.0, ref_epoch=epoch)
|
|
242
|
+
|
|
243
|
+
self.clocks[satellite] = clock
|
|
244
|
+
|
|
245
|
+
# Compute the bias for the updated clock
|
|
246
|
+
dt = (epoch - clock.ref_epoch).total_seconds() if clock.ref_epoch else 0.0
|
|
247
|
+
clock = Clock(clock.bias_s + clock.drift_s_per_s * dt, clock.drift_s_per_s, 0.0)
|
|
248
|
+
|
|
249
|
+
return clock
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class CodeBiases(ABC):
|
|
254
|
+
"""
|
|
255
|
+
This abstract class provides with an interface to provide with the code biases
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
@abstractmethod
|
|
259
|
+
def get_base_tgd(self) -> float:
|
|
260
|
+
"""
|
|
261
|
+
Get the base group delay
|
|
262
|
+
"""
|
|
263
|
+
return 0.0
|
|
264
|
+
|
|
265
|
+
@abstractmethod
|
|
266
|
+
def get_code_bias(self, channel: TrackingChannel) -> float:
|
|
267
|
+
"""
|
|
268
|
+
Get the code bias for the given channel
|
|
269
|
+
"""
|
|
270
|
+
return 0.0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ZeroCodeBiases(CodeBiases):
|
|
274
|
+
|
|
275
|
+
def get_base_tgd(self) -> float:
|
|
276
|
+
return super().get_base_tgd()
|
|
277
|
+
|
|
278
|
+
def get_code_bias(self, channel: TrackingChannel) -> float:
|
|
279
|
+
return super().get_code_bias(channel)
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class LEOCodeBiases(CodeBiases):
|
|
283
|
+
"""
|
|
284
|
+
Class to handle the code biases for the generic LEO constellation
|
|
285
|
+
"""
|
|
286
|
+
tgd_s: float
|
|
287
|
+
isc_s9c_s: float
|
|
288
|
+
|
|
289
|
+
def get_base_tgd(self) -> float:
|
|
290
|
+
return self.tgd_s
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def get_code_bias(self, channel: TrackingChannel) -> float:
|
|
294
|
+
|
|
295
|
+
if channel == TrackingChannel.from_string('9C'):
|
|
296
|
+
return self.isc_s9c_s
|
|
297
|
+
|
|
298
|
+
raise ValueError(f'Tracking channel [ {channel} ] not supported by LEO constellations')
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class Record(object):
|
|
303
|
+
epoch: datetime.datetime
|
|
304
|
+
sat: Satellite
|
|
305
|
+
channel: TrackingChannel
|
|
306
|
+
range: float
|
|
307
|
+
phase: float
|
|
308
|
+
doppler: float
|
|
309
|
+
snr: float
|
|
310
|
+
slip: int
|
|
311
|
+
|
|
312
|
+
def set_value(self, observable_type: ObservableType, observable_value: ObservableValue) -> None:
|
|
313
|
+
|
|
314
|
+
if observable_type.type == 'C':
|
|
315
|
+
self.range = observable_value.value
|
|
316
|
+
elif observable_type.type == 'L':
|
|
317
|
+
self.phase = observable_value.value
|
|
318
|
+
elif observable_type.type == 'D':
|
|
319
|
+
self.doppler = observable_value.value
|
|
320
|
+
elif observable_type.type == 'S':
|
|
321
|
+
self.snr = observable_value.value
|
|
322
|
+
else:
|
|
323
|
+
raise TypeError(f'Unrecognise observable type {observable_type.type}')
|
|
324
|
+
|
|
325
|
+
if observable_value.lli != 0:
|
|
326
|
+
self.slip = 1
|
|
327
|
+
|
|
328
|
+
def aslist(self) -> list:
|
|
329
|
+
|
|
330
|
+
return [self.epoch, self.sat.constellation.value, str(self.sat), str(self.channel),
|
|
331
|
+
f'{self.sat}{self.channel}', self.range, self.phase, self.doppler, self.snr, self.slip]
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def get_list_fieldnames() -> List[str]:
|
|
335
|
+
return ["epoch", "constellation", "sat", "channel", "signal", "range", "phase", "doppler", "snr", "slip"]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class EpochFlag(enum.Enum):
|
|
339
|
+
|
|
340
|
+
OK = 0
|
|
341
|
+
POWER_FAILURE = 1
|
|
342
|
+
MOVING_ANTENNA = 2
|
|
343
|
+
NEW_SITE = 3
|
|
344
|
+
HEADER_INFO = 4
|
|
345
|
+
EXTERNAL_EVENT = 5
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def get_from_line(line: str) -> 'EpochFlag':
|
|
349
|
+
"""
|
|
350
|
+
Extract the epoch flag from the incoming line
|
|
351
|
+
|
|
352
|
+
>>> EpochFlag.get_from_line("> 4 95")
|
|
353
|
+
<EpochFlag.HEADER_INFO: 4>
|
|
354
|
+
>>> EpochFlag.get_from_line("> 2023 08 03 12 00 8.0000000 0 38")
|
|
355
|
+
<EpochFlag.OK: 0>
|
|
356
|
+
>>> EpochFlag.get_from_line(None)
|
|
357
|
+
Traceback (most recent call last):
|
|
358
|
+
...
|
|
359
|
+
ValueError: The line [ None ] does not have an Epoch Flag
|
|
360
|
+
>>> EpochFlag.get_from_line("")
|
|
361
|
+
Traceback (most recent call last):
|
|
362
|
+
...
|
|
363
|
+
ValueError: The line [ ] does not have an Epoch Flag
|
|
364
|
+
>>> EpochFlag.get_from_line("> 2023 08 03 1")
|
|
365
|
+
Traceback (most recent call last):
|
|
366
|
+
...
|
|
367
|
+
ValueError: The line [ > 2023 08 03 1 ] does not have an Epoch Flag
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
INDEX = 31
|
|
371
|
+
|
|
372
|
+
if line and len(line) >= INDEX + 1 and line[0] == '>':
|
|
373
|
+
epoch_flag = int(line[INDEX])
|
|
374
|
+
return EpochFlag(epoch_flag)
|
|
375
|
+
|
|
376
|
+
raise ValueError(f'The line [ {line} ] does not have an Epoch Flag')
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class FilePeriod(enum.Enum):
|
|
380
|
+
|
|
381
|
+
DAILY = 86400
|
|
382
|
+
HOURLY = 3600
|
|
383
|
+
QUARTERLY = 900
|
|
384
|
+
UNDEFINED = 0
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def from_string(string):
|
|
388
|
+
"""
|
|
389
|
+
Get the FilePeriod from a string
|
|
390
|
+
|
|
391
|
+
>>> FilePeriod.from_string('daily')
|
|
392
|
+
<FilePeriod.DAILY: 86400>
|
|
393
|
+
|
|
394
|
+
>>> FilePeriod.from_string('DAILY')
|
|
395
|
+
<FilePeriod.DAILY: 86400>
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
if (string.lower() == 'daily'):
|
|
399
|
+
return FilePeriod.DAILY
|
|
400
|
+
elif (string.lower() == 'quarterly'):
|
|
401
|
+
return FilePeriod.QUARTERLY
|
|
402
|
+
elif (string.lower() == 'hourly'):
|
|
403
|
+
return FilePeriod.HOURLY
|
|
404
|
+
else:
|
|
405
|
+
return FilePeriod.UNDEFINED
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def list():
|
|
409
|
+
""" Return a list of the available valid periodicities """
|
|
410
|
+
return list([v.name for v in FilePeriod if v.value > 0])
|
|
411
|
+
|
|
412
|
+
def build_rinex3_epoch(self, epoch):
|
|
413
|
+
"""
|
|
414
|
+
Construct a Rinex-3-like epoch string
|
|
415
|
+
|
|
416
|
+
>>> epoch = datetime.datetime(2020, 5, 8, 9, 29, 20)
|
|
417
|
+
>>> FilePeriod.QUARTERLY.build_rinex3_epoch(epoch)
|
|
418
|
+
'20201290915_15M'
|
|
419
|
+
|
|
420
|
+
>>> FilePeriod.HOURLY.build_rinex3_epoch(epoch)
|
|
421
|
+
'20201290900_01H'
|
|
422
|
+
|
|
423
|
+
>>> FilePeriod.DAILY.build_rinex3_epoch(epoch)
|
|
424
|
+
'20201290000_01D'
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
hour = epoch.hour if self != FilePeriod.DAILY else 0
|
|
428
|
+
|
|
429
|
+
day_seconds = (epoch - epoch.combine(epoch, datetime.time())).total_seconds()
|
|
430
|
+
|
|
431
|
+
minute = get_quarter_str(day_seconds) if self == FilePeriod.QUARTERLY else 0
|
|
432
|
+
|
|
433
|
+
date_str = epoch.strftime('%Y%j')
|
|
434
|
+
|
|
435
|
+
return '{}{:02d}{:02d}_{}'.format(date_str, hour, minute, self)
|
|
436
|
+
|
|
437
|
+
def __str__(self):
|
|
438
|
+
|
|
439
|
+
if self.value == FilePeriod.DAILY.value:
|
|
440
|
+
return '01D'
|
|
441
|
+
elif self.value == FilePeriod.QUARTERLY.value:
|
|
442
|
+
return '15M'
|
|
443
|
+
elif self.value == FilePeriod.HOURLY.value:
|
|
444
|
+
return '01H'
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError('Undefined FilePeriod value')
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ------------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
def strftime(epoch, fmt):
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
>>> epoch = datetime.datetime(2019, 8, 3, 10, 10, 10)
|
|
455
|
+
>>> strftime(epoch, "ebre215${rinexhour}${rinexquarter}.19o")
|
|
456
|
+
'ebre215k00.19o'
|
|
457
|
+
|
|
458
|
+
>>> epoch = datetime.datetime(2019, 8, 3, 10, 50, 10)
|
|
459
|
+
>>> strftime(epoch, "ebre215${RINEXHOUR}${rinexQUARTER}.19o")
|
|
460
|
+
'ebre215k45.19o'
|
|
461
|
+
|
|
462
|
+
>>> epoch = datetime.datetime(2019, 8, 3, 0, 0, 0)
|
|
463
|
+
>>> strftime(epoch, "ebre215${rinexhour}${rinexquarter}.19o")
|
|
464
|
+
'ebre215a00.19o'
|
|
465
|
+
|
|
466
|
+
>>> epoch = datetime.datetime(2019, 8, 3, 23, 50, 10)
|
|
467
|
+
>>> strftime(epoch, "ebre215${rinexhour}${rinexquarter}.19o")
|
|
468
|
+
'ebre215x45.19o'
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
RINEX_HOUR = "abcdefghijklmnopqrstuvwxyz"
|
|
472
|
+
|
|
473
|
+
PATTERN_HOUR = re.compile(r"\$\{rinexhour\}", re.IGNORECASE)
|
|
474
|
+
PATTERN_QUARTER = re.compile(r"\$\{rinexquarter\}", re.IGNORECASE)
|
|
475
|
+
|
|
476
|
+
hour = RINEX_HOUR[epoch.hour]
|
|
477
|
+
quarter = get_quarter_str(epoch.minute * 60 + epoch.second)
|
|
478
|
+
|
|
479
|
+
fmt = PATTERN_HOUR.sub(f"{hour}", fmt)
|
|
480
|
+
fmt = PATTERN_QUARTER.sub(f"{quarter:02d}", fmt)
|
|
481
|
+
|
|
482
|
+
return fmt
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def get_quarter_str(seconds):
|
|
486
|
+
"""
|
|
487
|
+
Get the Rinex quarter string ("00", "15", "30", "45") for a given number of seconds
|
|
488
|
+
|
|
489
|
+
>>> get_quarter_str(100)
|
|
490
|
+
0
|
|
491
|
+
>>> get_quarter_str(920)
|
|
492
|
+
15
|
|
493
|
+
>>> get_quarter_str(1800)
|
|
494
|
+
30
|
|
495
|
+
>>> get_quarter_str(2900)
|
|
496
|
+
45
|
|
497
|
+
>>> get_quarter_str(3600 + 900)
|
|
498
|
+
15
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
mod_seconds = seconds % 3600
|
|
502
|
+
|
|
503
|
+
if mod_seconds < 900:
|
|
504
|
+
return 0
|
|
505
|
+
elif mod_seconds < 1800:
|
|
506
|
+
return 15
|
|
507
|
+
elif mod_seconds < 2700:
|
|
508
|
+
return 30
|
|
509
|
+
else:
|
|
510
|
+
return 45
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_channels(observables: List[ObservableType]) -> List[TrackingChannel]:
|
|
514
|
+
"""
|
|
515
|
+
Get the channel list from a list of observables
|
|
516
|
+
|
|
517
|
+
>>> C1C = ObservableType.from_string("C1C")
|
|
518
|
+
>>> L1C = ObservableType.from_string("L1C")
|
|
519
|
+
>>> C1W = ObservableType.from_string("C1W")
|
|
520
|
+
>>> C2W = ObservableType.from_string("C2W")
|
|
521
|
+
>>> L2W = ObservableType.from_string("L2W")
|
|
522
|
+
>>> C2L = ObservableType.from_string("C2L")
|
|
523
|
+
>>> L2L = ObservableType.from_string("L2L")
|
|
524
|
+
>>> C5Q = ObservableType.from_string("C5Q")
|
|
525
|
+
>>> L5Q = ObservableType.from_string("L5Q")
|
|
526
|
+
>>> get_channels([C1C, L1C, C1W, C2W, L2W, C2L, L2L, C5Q, L5Q])
|
|
527
|
+
[1C, 1W, 2W, 2L, 5Q]
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
res = []
|
|
531
|
+
for observable in observables:
|
|
532
|
+
channel = observable.channel
|
|
533
|
+
|
|
534
|
+
if channel not in res:
|
|
535
|
+
res.append(channel)
|
|
536
|
+
|
|
537
|
+
return res
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def get_obs_mapping(lines: List[str]) -> dict:
|
|
541
|
+
"""
|
|
542
|
+
Get the observable mappings for a constellation
|
|
543
|
+
|
|
544
|
+
>>> line = "G 9 C1C L1C C1W C2W L2W C2L L2L C5Q L5Q SYS / # / OBS TYPES"
|
|
545
|
+
>>> get_obs_mapping([line])
|
|
546
|
+
{'G': [C1C, L1C, C1W, C2W, L2W, C2L, L2L, C5Q, L5Q]}
|
|
547
|
+
|
|
548
|
+
>>> lines = ["G 20 C1C L1C D1C S1C C1W L1W D1W S1W C2W L2W D2W S2W C2L SYS / # / OBS TYPES", \
|
|
549
|
+
" L2L D2L S2L C5Q L5Q D5Q S5Q SYS / # / OBS TYPES"]
|
|
550
|
+
>>> get_obs_mapping(lines)
|
|
551
|
+
{'G': [C1C, L1C, D1C, S1C, C1W, L1W, D1W, S1W, C2W, L2W, D2W, S2W, C2L, L2L, D2L, S2L, C5Q, L5Q, D5Q, S5Q]}
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
constellation = None
|
|
555
|
+
values = None
|
|
556
|
+
|
|
557
|
+
for line in lines:
|
|
558
|
+
|
|
559
|
+
constellation_letter = line[0] if line[0] != ' ' else None
|
|
560
|
+
values_partial = [ObservableType.from_string(s) for s in line[6:60].split()]
|
|
561
|
+
|
|
562
|
+
if constellation_letter:
|
|
563
|
+
constellation = constellation_letter
|
|
564
|
+
values = values_partial
|
|
565
|
+
|
|
566
|
+
else:
|
|
567
|
+
values.extend(values_partial)
|
|
568
|
+
|
|
569
|
+
return {constellation: values}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def to_dataframe(rinex_file: str, station_name=None) -> pd.DataFrame:
|
|
573
|
+
"""
|
|
574
|
+
Convert a RINEX file to a pandas DataFrame structure
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
station_name_inferred = os.path.basename(rinex_file)[0:4]
|
|
578
|
+
|
|
579
|
+
with open(rinex_file, 'r', encoding='utf-8') as file:
|
|
580
|
+
|
|
581
|
+
constellation_observables = {}
|
|
582
|
+
records = []
|
|
583
|
+
|
|
584
|
+
# Header parsing
|
|
585
|
+
for line in file:
|
|
586
|
+
|
|
587
|
+
if "END OF HEADER" in line:
|
|
588
|
+
break
|
|
589
|
+
|
|
590
|
+
if RINEX_LINE_SYS_OBS_TYPES in line:
|
|
591
|
+
lines = [line]
|
|
592
|
+
|
|
593
|
+
n_observables = int(line[1:6])
|
|
594
|
+
n_extra_lines = (n_observables - 1) // 13 if n_observables > 13 else 0
|
|
595
|
+
for _ in range(n_extra_lines):
|
|
596
|
+
lines.append(next(file))
|
|
597
|
+
|
|
598
|
+
constellation_observables.update(get_obs_mapping(lines))
|
|
599
|
+
|
|
600
|
+
if "MARKER NAME" in line:
|
|
601
|
+
station_name_inferred = line[0:4]
|
|
602
|
+
|
|
603
|
+
# Body parsing
|
|
604
|
+
for line in file:
|
|
605
|
+
|
|
606
|
+
epoch_flag = EpochFlag.get_from_line(line)
|
|
607
|
+
|
|
608
|
+
if epoch_flag == EpochFlag.OK:
|
|
609
|
+
|
|
610
|
+
epoch, _, n_lines = _parse_rnx3_epoch(line)
|
|
611
|
+
|
|
612
|
+
for _ in range(n_lines):
|
|
613
|
+
|
|
614
|
+
line = next(file)
|
|
615
|
+
|
|
616
|
+
constellation = line[0]
|
|
617
|
+
|
|
618
|
+
observable_types = constellation_observables.get(constellation, None)
|
|
619
|
+
if observable_types is None:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
n_obs = len(observable_types)
|
|
623
|
+
channels = get_channels(observable_types)
|
|
624
|
+
satellite, observable_values = _parse_obs_line(line, n_obs)
|
|
625
|
+
|
|
626
|
+
epoch_records = [
|
|
627
|
+
Record(epoch, satellite, channel, math.nan, math.nan, math.nan, math.nan, 0)
|
|
628
|
+
for channel in channels]
|
|
629
|
+
|
|
630
|
+
# Create a dictionary with channels as keys and records as values
|
|
631
|
+
epoch_records_dict = {record.channel: record for record in epoch_records}
|
|
632
|
+
|
|
633
|
+
for observable_type, observable_value in zip(observable_types, observable_values):
|
|
634
|
+
|
|
635
|
+
record = epoch_records_dict.get(observable_type.channel, None)
|
|
636
|
+
if record is not None:
|
|
637
|
+
record.set_value(observable_type, observable_value)
|
|
638
|
+
|
|
639
|
+
records.extend(epoch_records)
|
|
640
|
+
|
|
641
|
+
else:
|
|
642
|
+
n_lines_to_skip = int(line[33:])
|
|
643
|
+
for _ in range(n_lines_to_skip):
|
|
644
|
+
line = next(file)
|
|
645
|
+
|
|
646
|
+
dataframe = pd.DataFrame([record.aslist() for record in records], columns=Record.get_list_fieldnames())
|
|
647
|
+
station_name_lowercase = str.lower(station_name if station_name is not None else station_name_inferred)
|
|
648
|
+
dataframe['station'] = station_name_lowercase
|
|
649
|
+
|
|
650
|
+
return dataframe
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class Obs:
|
|
654
|
+
|
|
655
|
+
def __init__(self, filename: str):
|
|
656
|
+
self.filename = filename
|
|
657
|
+
self.data = to_dataframe(self.filename)
|
|
658
|
+
timetags = sorted(set([ts.to_pydatetime() for ts in self.data['epoch']]))
|
|
659
|
+
if len(timetags) > 1:
|
|
660
|
+
self.interval = time.get_interval(timetags)
|
|
661
|
+
|
|
662
|
+
def compute_detrended_code_minus_carrier(self) -> pd.DataFrame:
|
|
663
|
+
self.count_cycle_slips()
|
|
664
|
+
|
|
665
|
+
grouped_data = self.data.groupby(['channel', 'sat', 'slipc'], group_keys=False)
|
|
666
|
+
self.data = grouped_data.apply(lambda df: self.__compute_grouped_detrended_cmc(df))
|
|
667
|
+
|
|
668
|
+
def count_cycle_slips(self):
|
|
669
|
+
grouped_data = self.data.groupby(['channel', 'sat'])
|
|
670
|
+
self.data['slipc'] = grouped_data['slip'].transform(lambda slip: slip.cumsum())
|
|
671
|
+
|
|
672
|
+
def __compute_grouped_detrended_cmc(self, grouped_df):
|
|
673
|
+
# grouped_df is rinex_obs.data grouped by 'channel', 'sat' and 'slipc'
|
|
674
|
+
const = grouped_df['constellation'].iloc[0]
|
|
675
|
+
constellation_id = ConstellationId.from_string(const)
|
|
676
|
+
|
|
677
|
+
chan = grouped_df['channel'].iloc[0]
|
|
678
|
+
band_frequency = RINEX_BAND_MAP[constellation_id][chan[0]]
|
|
679
|
+
wavelength = SPEED_OF_LIGHT / band_frequency
|
|
680
|
+
|
|
681
|
+
cmc = grouped_df['range'] - grouped_df['phase'] * wavelength
|
|
682
|
+
|
|
683
|
+
CMC_ROLL_WINDOW_SAMPLES = 600 / self.interval
|
|
684
|
+
if len(cmc) >= CMC_ROLL_WINDOW_SAMPLES:
|
|
685
|
+
trend = cmc.rolling(20).median()
|
|
686
|
+
else:
|
|
687
|
+
trend = cmc.mean()
|
|
688
|
+
|
|
689
|
+
grouped_df['cmc_detrended'] = cmc - trend
|
|
690
|
+
|
|
691
|
+
return grouped_df
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@dataclass
|
|
695
|
+
class NavBlock(ABC):
|
|
696
|
+
"""
|
|
697
|
+
Abstract class for RINEX Navigation block
|
|
698
|
+
"""
|
|
699
|
+
satellite: Satellite
|
|
700
|
+
epoch: datetime
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def csv_fields() -> str:
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
def to_csv(self) -> str:
|
|
707
|
+
# Get all field names from the __dataclass_fields__ attribute
|
|
708
|
+
field_names = [field.name for field in fields(self)]
|
|
709
|
+
|
|
710
|
+
# Get the value of each field from the instance dictionary
|
|
711
|
+
field_values = [getattr(self, field_name) for field_name in field_names]
|
|
712
|
+
|
|
713
|
+
# Convert field values to strings and join them with commas
|
|
714
|
+
body = ",".join(map(str, field_values))
|
|
715
|
+
|
|
716
|
+
return f'{self.get_type().value},{body}'
|
|
717
|
+
|
|
718
|
+
# Custom function to convert each NavBlock to a dict with string satellite
|
|
719
|
+
def to_dict(self):
|
|
720
|
+
block_dict = asdict(self)
|
|
721
|
+
block_dict['satellite'] = str(self.satellite) # Convert satellite to string
|
|
722
|
+
return block_dict
|
|
723
|
+
|
|
724
|
+
@abstractmethod
|
|
725
|
+
def get_type(self) -> EphType:
|
|
726
|
+
return None
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@dataclass
|
|
730
|
+
class GpsLnavNavBlock(NavBlock):
|
|
731
|
+
clock_bias_s: float
|
|
732
|
+
clock_drift_sps: float
|
|
733
|
+
clock_drift_rate_sps2: float
|
|
734
|
+
|
|
735
|
+
iode: int
|
|
736
|
+
crs: float
|
|
737
|
+
deltan: float
|
|
738
|
+
M0: float
|
|
739
|
+
|
|
740
|
+
cuc: float
|
|
741
|
+
e: float
|
|
742
|
+
cus: float
|
|
743
|
+
sqrtA: float
|
|
744
|
+
|
|
745
|
+
toe_sow: float
|
|
746
|
+
cic: float
|
|
747
|
+
OMEGA0: float
|
|
748
|
+
cis: float
|
|
749
|
+
|
|
750
|
+
i0: float
|
|
751
|
+
crc: float
|
|
752
|
+
omega: float
|
|
753
|
+
OMEGA_DOT: float
|
|
754
|
+
|
|
755
|
+
idot: float
|
|
756
|
+
codesL2: int
|
|
757
|
+
toe_week: int
|
|
758
|
+
l2p_flag: int
|
|
759
|
+
|
|
760
|
+
accuracy: float
|
|
761
|
+
health: int
|
|
762
|
+
tgd: float
|
|
763
|
+
iodc: int
|
|
764
|
+
|
|
765
|
+
tx_time_tow: float
|
|
766
|
+
fit_interval: int
|
|
767
|
+
|
|
768
|
+
def csv_fields(self) -> str:
|
|
769
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
770
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
771
|
+
{_IODE_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
772
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
773
|
+
{_TOE_SOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
774
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
775
|
+
{_IDOT_STR},{_CODESL2_STR},{_TOE_WEEK_STR},{_L2PFLAG_STR},\
|
|
776
|
+
{_ACCURACY_STR},{_HEALTH_STR},{_TGD_STR},{_IODC_STR},\
|
|
777
|
+
{_TX_TIME_TOW_STR},{_FIT_INTERVAL_STR}"
|
|
778
|
+
|
|
779
|
+
def get_type(self) -> EphType:
|
|
780
|
+
return EphType.GPS_LNAV
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
@dataclass
|
|
784
|
+
class GpsCnavNavBlock(NavBlock):
|
|
785
|
+
clock_bias_s: float
|
|
786
|
+
clock_drift_sps: float
|
|
787
|
+
clock_drift_rate_sps2: float
|
|
788
|
+
|
|
789
|
+
adot: float
|
|
790
|
+
crs: float
|
|
791
|
+
deltan: float
|
|
792
|
+
M0: float
|
|
793
|
+
|
|
794
|
+
cuc: float
|
|
795
|
+
e: float
|
|
796
|
+
cus: float
|
|
797
|
+
sqrtA: float
|
|
798
|
+
|
|
799
|
+
top: float
|
|
800
|
+
cic: float
|
|
801
|
+
OMEGA0: float
|
|
802
|
+
cis: float
|
|
803
|
+
|
|
804
|
+
i0: float
|
|
805
|
+
crc: float
|
|
806
|
+
omega: float
|
|
807
|
+
OMEGA_DOT: float
|
|
808
|
+
|
|
809
|
+
idot: float
|
|
810
|
+
deltan_dot: float
|
|
811
|
+
urai_ned0: float
|
|
812
|
+
urai_ned1: float
|
|
813
|
+
|
|
814
|
+
urai_ed: float
|
|
815
|
+
health: int
|
|
816
|
+
tgd: float
|
|
817
|
+
urai_ned2: float
|
|
818
|
+
|
|
819
|
+
isc_l1ca: float
|
|
820
|
+
isc_l2c: float
|
|
821
|
+
isc_l5i5: float
|
|
822
|
+
isc_l5q5: float
|
|
823
|
+
|
|
824
|
+
tx_time_tow: float
|
|
825
|
+
wn_op: int
|
|
826
|
+
|
|
827
|
+
def csv_fields(self) -> str:
|
|
828
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
829
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
830
|
+
{_ADOT_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
831
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
832
|
+
{_T_OP_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
833
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
834
|
+
{_IDOT_STR},{_DELTAN_DOT_STR},{_URAI_NED0_STR},{_URAI_NED1_STR},\
|
|
835
|
+
{_URAI_ED_STR},{_HEALTH_STR},{_TGD_STR},{_URAI_NED2_STR},\
|
|
836
|
+
{_ISC_L1CA_STR},{_ISC_L2C_STR},{_ISC_L5I5_STR},{_ISC_L5Q5_STR},\
|
|
837
|
+
{_TX_TIME_TOW_STR},{_WN_OP_STR}"
|
|
838
|
+
|
|
839
|
+
def get_type(self) -> EphType:
|
|
840
|
+
return EphType.GPS_CNAV
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@dataclass
|
|
844
|
+
class GpsCnv2NavBlock(NavBlock):
|
|
845
|
+
clock_bias_s: float
|
|
846
|
+
clock_drift_sps: float
|
|
847
|
+
clock_drift_rate_sps2: float
|
|
848
|
+
|
|
849
|
+
adot: float
|
|
850
|
+
crs: float
|
|
851
|
+
deltan: float
|
|
852
|
+
M0: float
|
|
853
|
+
|
|
854
|
+
cuc: float
|
|
855
|
+
e: float
|
|
856
|
+
cus: float
|
|
857
|
+
sqrtA: float
|
|
858
|
+
|
|
859
|
+
top: float
|
|
860
|
+
cic: float
|
|
861
|
+
OMEGA0: float
|
|
862
|
+
cis: float
|
|
863
|
+
|
|
864
|
+
i0: float
|
|
865
|
+
crc: float
|
|
866
|
+
omega: float
|
|
867
|
+
OMEGA_DOT: float
|
|
868
|
+
|
|
869
|
+
idot: float
|
|
870
|
+
deltan_dot: float
|
|
871
|
+
urai_ned0: float
|
|
872
|
+
urai_ned1: float
|
|
873
|
+
|
|
874
|
+
urai_ed: float
|
|
875
|
+
health: int
|
|
876
|
+
tgd: float
|
|
877
|
+
urai_ned2: float
|
|
878
|
+
|
|
879
|
+
isc_l1ca: float
|
|
880
|
+
isc_l2c: float
|
|
881
|
+
isc_l5i5: float
|
|
882
|
+
isc_l5q5: float
|
|
883
|
+
|
|
884
|
+
isc_l1cd: float
|
|
885
|
+
isc_l1cp: float
|
|
886
|
+
|
|
887
|
+
tx_time_tow: float
|
|
888
|
+
wn_op: int
|
|
889
|
+
|
|
890
|
+
def csv_fields(self) -> str:
|
|
891
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
892
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
893
|
+
{_ADOT_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
894
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
895
|
+
{_T_OP_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
896
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
897
|
+
{_IDOT_STR},{_DELTAN_DOT_STR},{_URAI_NED0_STR},{_URAI_NED1_STR},\
|
|
898
|
+
{_URAI_ED_STR},{_HEALTH_STR},{_TGD_STR},{_URAI_NED2_STR},\
|
|
899
|
+
{_ISC_L1CA_STR},{_ISC_L2C_STR},{_ISC_L5I5_STR},{_ISC_L5Q5_STR},\
|
|
900
|
+
{_ISC_L1CD_STR},{_ISC_L1CP_STR},\
|
|
901
|
+
{_TX_TIME_TOW_STR},{_WN_OP_STR}"
|
|
902
|
+
|
|
903
|
+
def get_type(self) -> EphType:
|
|
904
|
+
return EphType.GPS_CNV2
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
@dataclass
|
|
908
|
+
class GalNavBlock(NavBlock):
|
|
909
|
+
clock_bias_s: float
|
|
910
|
+
clock_drift_sps: float
|
|
911
|
+
clock_drift_rate_sps2: float
|
|
912
|
+
|
|
913
|
+
iodnav: float
|
|
914
|
+
crs: float
|
|
915
|
+
deltan: float
|
|
916
|
+
M0: float
|
|
917
|
+
|
|
918
|
+
cuc: float
|
|
919
|
+
e: float
|
|
920
|
+
cus: float
|
|
921
|
+
sqrtA: float
|
|
922
|
+
|
|
923
|
+
toe_gal_tow: int
|
|
924
|
+
cic: float
|
|
925
|
+
OMEGA0: float
|
|
926
|
+
cis: float
|
|
927
|
+
|
|
928
|
+
i0: float
|
|
929
|
+
crc: float
|
|
930
|
+
omega: float
|
|
931
|
+
OMEGA_DOT: float
|
|
932
|
+
|
|
933
|
+
idot: float
|
|
934
|
+
data_source: int
|
|
935
|
+
toe_gal_week: int
|
|
936
|
+
|
|
937
|
+
sisa: float
|
|
938
|
+
health: int
|
|
939
|
+
bgd_e5a_e1: float
|
|
940
|
+
bgd_e5b_e1: float
|
|
941
|
+
|
|
942
|
+
tx_time_tow: float
|
|
943
|
+
|
|
944
|
+
def csv_fields(self) -> str:
|
|
945
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
946
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
947
|
+
{_IODNAV_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
948
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
949
|
+
{_TOE_GAL_TOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
950
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
951
|
+
{_IDOT_STR},{_DATA_SOURCES_STR},{_TOE_GAL_WEEK_STR},,\
|
|
952
|
+
{_SISA_STR},{_HEALTH_STR},{_BGD_E5A_STR},{_BGD_E5B_STR},\
|
|
953
|
+
{_TX_TIME_TOW_STR}"
|
|
954
|
+
|
|
955
|
+
def get_type(self) -> EphType:
|
|
956
|
+
INAV_MASK = 0b101
|
|
957
|
+
FNAV_MASK = 0b10
|
|
958
|
+
|
|
959
|
+
if self.data_source & INAV_MASK:
|
|
960
|
+
return EphType.GAL_INAV
|
|
961
|
+
elif self.data_source & FNAV_MASK:
|
|
962
|
+
return EphType.GAL_FNAV
|
|
963
|
+
|
|
964
|
+
raise ValueError(f'Cannot extract the Galileo Eph type from the data sources [ {self.data_source} ]')
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
@dataclass
|
|
968
|
+
class BdsDNavBlock(NavBlock):
|
|
969
|
+
clock_bias_s: float
|
|
970
|
+
clock_drift_sps: float
|
|
971
|
+
clock_drift_rate_sps2: float
|
|
972
|
+
|
|
973
|
+
aode: int
|
|
974
|
+
crs: float
|
|
975
|
+
deltan: float
|
|
976
|
+
M0: float
|
|
977
|
+
|
|
978
|
+
cuc: float
|
|
979
|
+
e: float
|
|
980
|
+
cus: float
|
|
981
|
+
sqrtA: float
|
|
982
|
+
|
|
983
|
+
toe_bdt_tow: int
|
|
984
|
+
cic: float
|
|
985
|
+
OMEGA0: float
|
|
986
|
+
cis: float
|
|
987
|
+
|
|
988
|
+
i0: float
|
|
989
|
+
crc: float
|
|
990
|
+
omega: float
|
|
991
|
+
OMEGA_DOT: float
|
|
992
|
+
|
|
993
|
+
idot: float
|
|
994
|
+
toe_bdt_week: int
|
|
995
|
+
|
|
996
|
+
accuracy: float
|
|
997
|
+
satH1: int
|
|
998
|
+
tgd1: float
|
|
999
|
+
tgd2: float
|
|
1000
|
+
|
|
1001
|
+
tx_time_tow: float
|
|
1002
|
+
aodc: int
|
|
1003
|
+
|
|
1004
|
+
eph_type: str
|
|
1005
|
+
|
|
1006
|
+
def csv_fields(self) -> str:
|
|
1007
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
1008
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
1009
|
+
{_AODE_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
1010
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
1011
|
+
{_TOE_BDT_TOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
1012
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
1013
|
+
{_IDOT_STR},{_TOE_BDT_WEEK_STR},\
|
|
1014
|
+
{_ACCURACY_STR},{_SATH1_STR},{_TGD1_STR},{_TGD2_STR},\
|
|
1015
|
+
{_TX_TIME_TOW_STR},{_AODC_STR}"
|
|
1016
|
+
|
|
1017
|
+
def get_type(self) -> EphType:
|
|
1018
|
+
return EphType.from_string(self.eph_type)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@dataclass
|
|
1022
|
+
class BdsCnv1NavBlock(NavBlock):
|
|
1023
|
+
"""
|
|
1024
|
+
BEIDOU Navigation message on Beidou-3 B1C signal
|
|
1025
|
+
"""
|
|
1026
|
+
clock_bias_s: float
|
|
1027
|
+
clock_drift_sps: float
|
|
1028
|
+
clock_drift_rate_sps2: float
|
|
1029
|
+
|
|
1030
|
+
adot: float
|
|
1031
|
+
crs: float
|
|
1032
|
+
deltan: float
|
|
1033
|
+
M0: float
|
|
1034
|
+
|
|
1035
|
+
cuc: float
|
|
1036
|
+
e: float
|
|
1037
|
+
cus: float
|
|
1038
|
+
sqrtA: float
|
|
1039
|
+
|
|
1040
|
+
toe_bdt_tow: int
|
|
1041
|
+
cic: float
|
|
1042
|
+
OMEGA0: float
|
|
1043
|
+
cis: float
|
|
1044
|
+
|
|
1045
|
+
i0: float
|
|
1046
|
+
crc: float
|
|
1047
|
+
omega: float
|
|
1048
|
+
OMEGA_DOT: float
|
|
1049
|
+
|
|
1050
|
+
idot: float
|
|
1051
|
+
deltan_dot: float
|
|
1052
|
+
sattype: int
|
|
1053
|
+
t_op: float
|
|
1054
|
+
|
|
1055
|
+
sisai_oe: float
|
|
1056
|
+
sisai_ocb: float
|
|
1057
|
+
sisai_oc1: float
|
|
1058
|
+
sisai_oc2: float
|
|
1059
|
+
|
|
1060
|
+
isc_b1cd: float
|
|
1061
|
+
tgd_b1cp: float
|
|
1062
|
+
tgd_b2ap: float
|
|
1063
|
+
|
|
1064
|
+
sismai: float
|
|
1065
|
+
health: int
|
|
1066
|
+
b1c_integrity_flags: int
|
|
1067
|
+
iodc: int
|
|
1068
|
+
|
|
1069
|
+
tx_time_tow: float
|
|
1070
|
+
iode: int
|
|
1071
|
+
|
|
1072
|
+
def csv_fields(self) -> str:
|
|
1073
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
1074
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
1075
|
+
{_ADOT_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
1076
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
1077
|
+
{_TOE_BDT_TOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
1078
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
1079
|
+
{_IDOT_STR},{_DELTAN_DOT_STR},{_SAT_TYPE_STR},{_T_OP_STR},\
|
|
1080
|
+
{_SISAI_OE_STR},{_SISAI_OCB_STR},{_SISAI_OC1_STR},{_SISAI_OC2_STR},\
|
|
1081
|
+
{_ISC_B1CD_STR},{_TGD_B1CP},{_TGD_B2AP},\
|
|
1082
|
+
{_SISMAI_STR},{_HEALTH_STR},{_B1C_INTEGRITY_FLAGS_STR},{_IODC_STR},\
|
|
1083
|
+
{_TX_TIME_TOW_STR},{_IODE_STR}"
|
|
1084
|
+
|
|
1085
|
+
def get_type(self) -> EphType:
|
|
1086
|
+
return EphType.BDS_CNV1
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
@dataclass
|
|
1090
|
+
class BdsCnv2NavBlock(NavBlock):
|
|
1091
|
+
"""
|
|
1092
|
+
BEIDOU Navigation message on Beidou-3 B2a signal
|
|
1093
|
+
"""
|
|
1094
|
+
clock_bias_s: float
|
|
1095
|
+
clock_drift_sps: float
|
|
1096
|
+
clock_drift_rate_sps2: float
|
|
1097
|
+
|
|
1098
|
+
adot: float
|
|
1099
|
+
crs: float
|
|
1100
|
+
deltan: float
|
|
1101
|
+
M0: float
|
|
1102
|
+
|
|
1103
|
+
cuc: float
|
|
1104
|
+
e: float
|
|
1105
|
+
cus: float
|
|
1106
|
+
sqrtA: float
|
|
1107
|
+
|
|
1108
|
+
toe_bdt_tow: int
|
|
1109
|
+
cic: float
|
|
1110
|
+
OMEGA0: float
|
|
1111
|
+
cis: float
|
|
1112
|
+
|
|
1113
|
+
i0: float
|
|
1114
|
+
crc: float
|
|
1115
|
+
omega: float
|
|
1116
|
+
OMEGA_DOT: float
|
|
1117
|
+
|
|
1118
|
+
idot: float
|
|
1119
|
+
deltan_dot: float
|
|
1120
|
+
sattype: int
|
|
1121
|
+
t_op: float
|
|
1122
|
+
|
|
1123
|
+
sisai_oe: float
|
|
1124
|
+
sisai_ocb: float
|
|
1125
|
+
sisai_oc1: float
|
|
1126
|
+
sisai_oc2: float
|
|
1127
|
+
|
|
1128
|
+
isc_b2ad: float
|
|
1129
|
+
tgd_b1cp: float
|
|
1130
|
+
tgd_b2ap: float
|
|
1131
|
+
|
|
1132
|
+
sismai: float
|
|
1133
|
+
health: int
|
|
1134
|
+
b2a_integrity_flags: int
|
|
1135
|
+
iodc: int
|
|
1136
|
+
|
|
1137
|
+
tx_time_tow: float
|
|
1138
|
+
iode: int
|
|
1139
|
+
|
|
1140
|
+
def csv_fields(self) -> str:
|
|
1141
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
1142
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
1143
|
+
{_ADOT_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
1144
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
1145
|
+
{_TOE_BDT_TOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
1146
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
1147
|
+
{_IDOT_STR},{_DELTAN_DOT_STR},{_SAT_TYPE_STR},{_T_OP_STR},\
|
|
1148
|
+
{_SISAI_OE_STR},{_SISAI_OCB_STR},{_SISAI_OC1_STR},{_SISAI_OC2_STR},\
|
|
1149
|
+
{_ISC_B2AD_STR},{_TGD_B1CP},{_TGD_B2AP},\
|
|
1150
|
+
{_SISMAI_STR},{_HEALTH_STR},{_B2A_INTEGRITY_FLAGS_STR},{_IODC_STR},\
|
|
1151
|
+
{_TX_TIME_TOW_STR},{_IODE_STR}"
|
|
1152
|
+
|
|
1153
|
+
def get_type(self) -> EphType:
|
|
1154
|
+
return EphType.BDS_CNV2
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@dataclass
|
|
1158
|
+
class BdsCnv3NavBlock(NavBlock):
|
|
1159
|
+
"""
|
|
1160
|
+
BEIDOU Navigation message on Beidou-3 B2b signal
|
|
1161
|
+
"""
|
|
1162
|
+
clock_bias_s: float
|
|
1163
|
+
clock_drift_sps: float
|
|
1164
|
+
clock_drift_rate_sps2: float
|
|
1165
|
+
|
|
1166
|
+
adot: float
|
|
1167
|
+
crs: float
|
|
1168
|
+
deltan: float
|
|
1169
|
+
M0: float
|
|
1170
|
+
|
|
1171
|
+
cuc: float
|
|
1172
|
+
e: float
|
|
1173
|
+
cus: float
|
|
1174
|
+
sqrtA: float
|
|
1175
|
+
|
|
1176
|
+
toe_bdt_tow: int
|
|
1177
|
+
cic: float
|
|
1178
|
+
OMEGA0: float
|
|
1179
|
+
cis: float
|
|
1180
|
+
|
|
1181
|
+
i0: float
|
|
1182
|
+
crc: float
|
|
1183
|
+
omega: float
|
|
1184
|
+
OMEGA_DOT: float
|
|
1185
|
+
|
|
1186
|
+
idot: float
|
|
1187
|
+
deltan_dot: float
|
|
1188
|
+
sattype: int
|
|
1189
|
+
t_op: float
|
|
1190
|
+
|
|
1191
|
+
sisai_oe: float
|
|
1192
|
+
sisai_ocb: float
|
|
1193
|
+
sisai_oc1: float
|
|
1194
|
+
sisai_oc2: float
|
|
1195
|
+
|
|
1196
|
+
sismai: float
|
|
1197
|
+
health: int
|
|
1198
|
+
b2b_integrity_flags: int
|
|
1199
|
+
tgd_b2bi: int
|
|
1200
|
+
|
|
1201
|
+
tx_time_tow: float
|
|
1202
|
+
|
|
1203
|
+
def csv_fields(self) -> str:
|
|
1204
|
+
return f"{self.get_type().value},{_SAT_STR},{_EPOCH_STR},\
|
|
1205
|
+
{_CLK_BIAS_STR},{_CLK_DRIFT_STR},{_CLK_DRIFT_RATE_STR},\
|
|
1206
|
+
{_ADOT_STR},{_CRS_STR},{_DELTAN_STR},{_M0_STR},\
|
|
1207
|
+
{_CUC_STR},{_E_STR},{_CUS_STR},{_SQRTA_STR},\
|
|
1208
|
+
{_TOE_BDT_TOW_STR},{_CIC_STR},{_OMEGA0_STR},{_CIS_STR},\
|
|
1209
|
+
{_I0_STR},{_CRC_STR},{_OMEGA_STR},{_OMEGA_DOT_STR},\
|
|
1210
|
+
{_IDOT_STR},{_DELTAN_DOT_STR},{_SAT_TYPE_STR},{_T_OP_STR},\
|
|
1211
|
+
{_SISAI_OE_STR},{_SISAI_OCB_STR},{_SISAI_OC1_STR},{_SISAI_OC2_STR},\
|
|
1212
|
+
{_SISMAI_STR},{_HEALTH_STR},{_B2B_INTEGRITY_FLAGS_STR},{_TGD_B2BI},\
|
|
1213
|
+
{_TX_TIME_TOW_STR},{_IODE_STR}"
|
|
1214
|
+
|
|
1215
|
+
def get_type(self) -> EphType:
|
|
1216
|
+
return EphType.BDS_CNV2
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
@dataclass
|
|
1220
|
+
class RawNavBlock(object):
|
|
1221
|
+
satellite: Satellite
|
|
1222
|
+
epoch: datetime.datetime
|
|
1223
|
+
lines: List[str]
|
|
1224
|
+
|
|
1225
|
+
def __repr__(self):
|
|
1226
|
+
return '\n'.join(self.lines)
|
|
1227
|
+
|
|
1228
|
+
def __lt__(self, other):
|
|
1229
|
+
|
|
1230
|
+
if self.epoch != other.epoch:
|
|
1231
|
+
return self.epoch < other.epoch
|
|
1232
|
+
|
|
1233
|
+
elif self.satellite.constellation != other.satellite.constellation:
|
|
1234
|
+
return self.satellite.constellation < other.satellite.constellation
|
|
1235
|
+
|
|
1236
|
+
return self.satellite.prn < other.satellite.prn
|
|
1237
|
+
|
|
1238
|
+
def to_rinex2(self) -> str:
|
|
1239
|
+
|
|
1240
|
+
out = ""
|
|
1241
|
+
|
|
1242
|
+
id = self.satellite.prn
|
|
1243
|
+
if id > 99:
|
|
1244
|
+
raise ValueError('Cannot write a Rinex 2 nav block for satellite with id > 99')
|
|
1245
|
+
|
|
1246
|
+
epoch_line = f'{id:2d}'
|
|
1247
|
+
clock_str = self.lines[1][23:]
|
|
1248
|
+
out = out + f'{epoch_line} {self.epoch.strftime("%y %m %d %H %M %S.0")}{clock_str}\n'
|
|
1249
|
+
for brdc_line in self.lines[2:]:
|
|
1250
|
+
out = out + f'{brdc_line[1:]}\n'
|
|
1251
|
+
# print extra lines
|
|
1252
|
+
out = out + ' 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00\n'
|
|
1253
|
+
out = out + f'{self.lines[4][1:23]} 0.000000000000e+00'
|
|
1254
|
+
|
|
1255
|
+
return out
|
|
1256
|
+
|
|
1257
|
+
def to_csv(self) -> str:
|
|
1258
|
+
pass
|
|
1259
|
+
|
|
1260
|
+
def to_nav_block(self) -> NavBlock:
|
|
1261
|
+
"""
|
|
1262
|
+
Convert to Navigation Block
|
|
1263
|
+
"""
|
|
1264
|
+
|
|
1265
|
+
# Get the type of EPH block
|
|
1266
|
+
eph_type = _get_eph_type_from_rinex4_nav_block_header(self.lines[0])
|
|
1267
|
+
if eph_type == EphType.GPS_LNAV:
|
|
1268
|
+
return self._to_GPS_LNAV()
|
|
1269
|
+
elif eph_type == EphType.GPS_CNAV:
|
|
1270
|
+
return self._to_GPS_CNAV()
|
|
1271
|
+
elif eph_type == EphType.GPS_CNV2:
|
|
1272
|
+
return self._to_GPS_CNV2()
|
|
1273
|
+
elif eph_type == EphType.GAL_FNAV or eph_type == EphType.GAL_INAV:
|
|
1274
|
+
return self._to_GAL()
|
|
1275
|
+
elif eph_type == EphType.BDS_D1 or eph_type == EphType.BDS_D2:
|
|
1276
|
+
return self._to_BDS_D(eph_type)
|
|
1277
|
+
elif eph_type == EphType.BDS_CNV1:
|
|
1278
|
+
return self._to_BDS_CNV1()
|
|
1279
|
+
elif eph_type == EphType.BDS_CNV2:
|
|
1280
|
+
return self._to_BDS_CNV2()
|
|
1281
|
+
elif eph_type == EphType.BDS_CNV3:
|
|
1282
|
+
return self._to_BDS_CNV3()
|
|
1283
|
+
|
|
1284
|
+
raise ValueError(f'There is no NavBlock generator for [ {eph_type} ]')
|
|
1285
|
+
|
|
1286
|
+
def _to_GPS_LNAV(self) -> GpsLnavNavBlock:
|
|
1287
|
+
|
|
1288
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1289
|
+
iode, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1290
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1291
|
+
toe_sow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1292
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1293
|
+
idot, codesL2, toe_week, l2p_flag = _parse_nav_orb_line(self.lines[6])
|
|
1294
|
+
accuracy, health, tgd, iodc = _parse_nav_orb_line(self.lines[7])
|
|
1295
|
+
tx_time_tow, fit_interval, _, _ = _parse_nav_orb_line(self.lines[8])
|
|
1296
|
+
|
|
1297
|
+
if fit_interval is None:
|
|
1298
|
+
fit_interval = 0
|
|
1299
|
+
|
|
1300
|
+
return GpsLnavNavBlock(Satellite.from_string(sat_str),
|
|
1301
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1302
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1303
|
+
iode, crs, deltan, M0,
|
|
1304
|
+
cuc, e, cus, sqrtA,
|
|
1305
|
+
toe_sow, cic, OMEGA0, cis,
|
|
1306
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1307
|
+
idot, int(codesL2), int(toe_week), int(l2p_flag),
|
|
1308
|
+
accuracy, int(health), tgd, int(iodc),
|
|
1309
|
+
tx_time_tow, int(fit_interval))
|
|
1310
|
+
|
|
1311
|
+
def _to_GPS_CNAV(self) -> GpsCnavNavBlock:
|
|
1312
|
+
|
|
1313
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1314
|
+
adot, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1315
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1316
|
+
top, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1317
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1318
|
+
idot, deltan_dot, urai_ned0, urai_ned1 = _parse_nav_orb_line(self.lines[6])
|
|
1319
|
+
urai_ed, health, tgd, urai_ned = _parse_nav_orb_line(self.lines[7])
|
|
1320
|
+
isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5 = _parse_nav_orb_line(self.lines[8])
|
|
1321
|
+
tx_time_tow, wn_op, _, _ = _parse_nav_orb_line(self.lines[9])
|
|
1322
|
+
|
|
1323
|
+
return GpsCnavNavBlock(Satellite.from_string(sat_str),
|
|
1324
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1325
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1326
|
+
adot, crs, deltan, M0,
|
|
1327
|
+
cuc, e, cus, sqrtA,
|
|
1328
|
+
top, cic, OMEGA0, cis,
|
|
1329
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1330
|
+
idot, deltan_dot, urai_ned0, urai_ned1,
|
|
1331
|
+
urai_ed, health, tgd, urai_ned,
|
|
1332
|
+
isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5,
|
|
1333
|
+
tx_time_tow, int(wn_op))
|
|
1334
|
+
|
|
1335
|
+
def _to_GPS_CNV2(self) -> GpsCnv2NavBlock:
|
|
1336
|
+
|
|
1337
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1338
|
+
adot, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1339
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1340
|
+
top, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1341
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1342
|
+
idot, deltan_dot, urai_ned0, urai_ned1 = _parse_nav_orb_line(self.lines[6])
|
|
1343
|
+
urai_ed, health, tgd, urai_ned = _parse_nav_orb_line(self.lines[7])
|
|
1344
|
+
isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5 = _parse_nav_orb_line(self.lines[8])
|
|
1345
|
+
isc_l1cd, isc_l1cp, _, _ = _parse_nav_orb_line(self.lines[9])
|
|
1346
|
+
tx_time_tow, wn_op, _, _ = _parse_nav_orb_line(self.lines[10])
|
|
1347
|
+
|
|
1348
|
+
return GpsCnv2NavBlock(Satellite.from_string(sat_str),
|
|
1349
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1350
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1351
|
+
adot, crs, deltan, M0,
|
|
1352
|
+
cuc, e, cus, sqrtA,
|
|
1353
|
+
top, cic, OMEGA0, cis,
|
|
1354
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1355
|
+
idot, deltan_dot, urai_ned0, urai_ned1,
|
|
1356
|
+
urai_ed, health, tgd, urai_ned,
|
|
1357
|
+
isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5,
|
|
1358
|
+
isc_l1cd, isc_l1cp,
|
|
1359
|
+
tx_time_tow, int(wn_op))
|
|
1360
|
+
|
|
1361
|
+
def _to_GAL(self) -> GalNavBlock:
|
|
1362
|
+
|
|
1363
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1364
|
+
iodnav, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1365
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1366
|
+
toe_tow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1367
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1368
|
+
idot, datasources, toe_week, _ = _parse_nav_orb_line(self.lines[6])
|
|
1369
|
+
sisa, health, bgd_e5a, bgd_e5b = _parse_nav_orb_line(self.lines[7])
|
|
1370
|
+
tx_tm, _, _, _ = _parse_nav_orb_line(self.lines[8])
|
|
1371
|
+
|
|
1372
|
+
return GalNavBlock(Satellite.from_string(sat_str),
|
|
1373
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1374
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1375
|
+
int(iodnav), crs, deltan, M0,
|
|
1376
|
+
cuc, e, cus, sqrtA,
|
|
1377
|
+
toe_tow, cic, OMEGA0, cis,
|
|
1378
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1379
|
+
idot, int(datasources), int(toe_week),
|
|
1380
|
+
sisa, health, bgd_e5a, bgd_e5b,
|
|
1381
|
+
tx_tm)
|
|
1382
|
+
|
|
1383
|
+
def _to_BDS_D(self, eph_type: EphType) -> BdsDNavBlock:
|
|
1384
|
+
|
|
1385
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1386
|
+
aode, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1387
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1388
|
+
toe_tow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1389
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1390
|
+
idot, _, toe_week, _ = _parse_nav_orb_line(self.lines[6])
|
|
1391
|
+
accuracy, satH1, tgd1, tgd2 = _parse_nav_orb_line(self.lines[7])
|
|
1392
|
+
tx_tm, aodc, _, _ = _parse_nav_orb_line(self.lines[8])
|
|
1393
|
+
|
|
1394
|
+
return BdsDNavBlock(Satellite.from_string(sat_str),
|
|
1395
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1396
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1397
|
+
int(aode), crs, deltan, M0,
|
|
1398
|
+
cuc, e, cus, sqrtA,
|
|
1399
|
+
toe_tow, cic, OMEGA0, cis,
|
|
1400
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1401
|
+
idot, int(toe_week),
|
|
1402
|
+
accuracy, int(satH1), tgd1, tgd2,
|
|
1403
|
+
tx_tm, aodc, eph_type.value)
|
|
1404
|
+
|
|
1405
|
+
def _to_BDS_CNV1(self) -> BdsCnv1NavBlock:
|
|
1406
|
+
|
|
1407
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1408
|
+
adot, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1409
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1410
|
+
toe_tow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1411
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1412
|
+
idot, deltan_dot, sattype, t_op = _parse_nav_orb_line(self.lines[6])
|
|
1413
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2 = _parse_nav_orb_line(self.lines[7])
|
|
1414
|
+
isc_b1cd, _, tgd_b1cp, tgd_b2ap = _parse_nav_orb_line(self.lines[8])
|
|
1415
|
+
sismai, health, b1c_integrity_flags, iodc = _parse_nav_orb_line(self.lines[9])
|
|
1416
|
+
tx_time_tow, _, _, iode = _parse_nav_orb_line(self.lines[10])
|
|
1417
|
+
|
|
1418
|
+
return BdsCnv1NavBlock(Satellite.from_string(sat_str),
|
|
1419
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1420
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1421
|
+
adot, crs, deltan, M0,
|
|
1422
|
+
cuc, e, cus, sqrtA,
|
|
1423
|
+
toe_tow, cic, OMEGA0, cis,
|
|
1424
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1425
|
+
idot, deltan_dot, int(sattype), t_op,
|
|
1426
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2,
|
|
1427
|
+
isc_b1cd, tgd_b1cp, tgd_b2ap,
|
|
1428
|
+
sismai, int(health), int(b1c_integrity_flags), int(iodc),
|
|
1429
|
+
tx_time_tow, int(iode))
|
|
1430
|
+
|
|
1431
|
+
def _to_BDS_CNV2(self) -> BdsCnv2NavBlock:
|
|
1432
|
+
|
|
1433
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1434
|
+
adot, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1435
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1436
|
+
toe_tow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1437
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1438
|
+
idot, deltan_dot, sattype, t_op = _parse_nav_orb_line(self.lines[6])
|
|
1439
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2 = _parse_nav_orb_line(self.lines[7])
|
|
1440
|
+
isc_b2ad, _, tgd_b1cp, tgd_b2ap = _parse_nav_orb_line(self.lines[8])
|
|
1441
|
+
sismai, health, b2a_integrity_flags, iodc = _parse_nav_orb_line(self.lines[9])
|
|
1442
|
+
tx_time_tow, _, _, iode = _parse_nav_orb_line(self.lines[10])
|
|
1443
|
+
|
|
1444
|
+
return BdsCnv2NavBlock(Satellite.from_string(sat_str),
|
|
1445
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1446
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1447
|
+
adot, crs, deltan, M0,
|
|
1448
|
+
cuc, e, cus, sqrtA,
|
|
1449
|
+
toe_tow, cic, OMEGA0, cis,
|
|
1450
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1451
|
+
idot, deltan_dot, int(sattype), t_op,
|
|
1452
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2,
|
|
1453
|
+
isc_b2ad, tgd_b1cp, tgd_b2ap,
|
|
1454
|
+
sismai, int(health), int(b2a_integrity_flags), int(iodc),
|
|
1455
|
+
tx_time_tow, int(iode))
|
|
1456
|
+
|
|
1457
|
+
def _to_BDS_CNV3(self) -> BdsCnv3NavBlock:
|
|
1458
|
+
|
|
1459
|
+
sat_str, year, month, day, hour, min, sec, clk_bias, clk_drift, clk_drift_rate = _parse_nav_epoch_line(self.lines[1])
|
|
1460
|
+
adot, crs, deltan, M0 = _parse_nav_orb_line(self.lines[2])
|
|
1461
|
+
cuc, e, cus, sqrtA = _parse_nav_orb_line(self.lines[3])
|
|
1462
|
+
toe_tow, cic, OMEGA0, cis = _parse_nav_orb_line(self.lines[4])
|
|
1463
|
+
i0, crc, omega, OMEGA_DOT = _parse_nav_orb_line(self.lines[5])
|
|
1464
|
+
idot, deltan_dot, sattype, t_op = _parse_nav_orb_line(self.lines[6])
|
|
1465
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2 = _parse_nav_orb_line(self.lines[7])
|
|
1466
|
+
sismai, health, b2b_integrity_flags, tgd_b2ap = _parse_nav_orb_line(self.lines[8])
|
|
1467
|
+
tx_time_tow, _, _, iode = _parse_nav_orb_line(self.lines[9])
|
|
1468
|
+
|
|
1469
|
+
return BdsCnv3NavBlock(Satellite.from_string(sat_str),
|
|
1470
|
+
datetime.datetime(year, month, day, hour, min, sec),
|
|
1471
|
+
clk_bias, clk_drift, clk_drift_rate,
|
|
1472
|
+
adot, crs, deltan, M0,
|
|
1473
|
+
cuc, e, cus, sqrtA,
|
|
1474
|
+
toe_tow, cic, OMEGA0, cis,
|
|
1475
|
+
i0, crc, omega, OMEGA_DOT,
|
|
1476
|
+
idot, deltan_dot, int(sattype), t_op,
|
|
1477
|
+
sisai_oe, sisai_ocb, sisai_oc1, sisai_oc2,
|
|
1478
|
+
sismai, int(health), int(b2b_integrity_flags), tgd_b2ap,
|
|
1479
|
+
tx_time_tow)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
class Nav(object):
|
|
1483
|
+
"""
|
|
1484
|
+
Class that holds Rinex Navigation data
|
|
1485
|
+
"""
|
|
1486
|
+
|
|
1487
|
+
RINEX4_EPH_BLOCK_LINES_LEO = 8
|
|
1488
|
+
|
|
1489
|
+
RINEX4_EPH_BLOCK_LINES = {
|
|
1490
|
+
EphType.GPS_LNAV: 8,
|
|
1491
|
+
EphType.GPS_CNAV: 9,
|
|
1492
|
+
EphType.GPS_CNV2: 10,
|
|
1493
|
+
EphType.GAL_INAV: 8,
|
|
1494
|
+
EphType.GAL_FNAV: 8,
|
|
1495
|
+
EphType.GLO_FDMA: 5,
|
|
1496
|
+
EphType.QZS_LNAV: 8,
|
|
1497
|
+
EphType.QZS_CNAV: 9,
|
|
1498
|
+
EphType.QZS_CNV2: 10,
|
|
1499
|
+
EphType.BDS_D1: 8,
|
|
1500
|
+
EphType.BDS_D2: 8,
|
|
1501
|
+
EphType.BDS_CNV1: 10,
|
|
1502
|
+
EphType.BDS_CNV2: 10,
|
|
1503
|
+
EphType.BDS_CNV3: 9,
|
|
1504
|
+
EphType.SBS: 4,
|
|
1505
|
+
EphType.IRN_LNAV: 8,
|
|
1506
|
+
EphType.LEO: RINEX4_EPH_BLOCK_LINES_LEO,
|
|
1507
|
+
EphType.SPIRE: RINEX4_EPH_BLOCK_LINES_LEO,
|
|
1508
|
+
EphType.STARLINK: RINEX4_EPH_BLOCK_LINES_LEO,
|
|
1509
|
+
EphType.ONEWEB: RINEX4_EPH_BLOCK_LINES_LEO,
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
def __init__(self, file: Union[str, IO]):
|
|
1513
|
+
self.blocks = Nav._load(file)
|
|
1514
|
+
|
|
1515
|
+
@staticmethod
|
|
1516
|
+
@process_filename_or_file_handler('r')
|
|
1517
|
+
def _load(fh: IO) -> List['Nav']:
|
|
1518
|
+
"""
|
|
1519
|
+
Load from a stream of data
|
|
1520
|
+
"""
|
|
1521
|
+
|
|
1522
|
+
blocks = []
|
|
1523
|
+
|
|
1524
|
+
# header
|
|
1525
|
+
line = fh.readline()
|
|
1526
|
+
|
|
1527
|
+
if not line.startswith(' 4'):
|
|
1528
|
+
raise ValueError(f'Unsupported RINEX Nav version [ {line[5:6]} ] ')
|
|
1529
|
+
|
|
1530
|
+
# body
|
|
1531
|
+
while True:
|
|
1532
|
+
line = fh.readline().rstrip()
|
|
1533
|
+
if line is None or len(line) == 0:
|
|
1534
|
+
break
|
|
1535
|
+
|
|
1536
|
+
if line.startswith('> EOP'):
|
|
1537
|
+
skip_lines(fh, 3)
|
|
1538
|
+
continue
|
|
1539
|
+
|
|
1540
|
+
elif line.startswith('> STO'):
|
|
1541
|
+
skip_lines(fh, 2)
|
|
1542
|
+
continue
|
|
1543
|
+
|
|
1544
|
+
elif line.startswith('> ION') and line.endswith('> LNAV'):
|
|
1545
|
+
skip_lines(fh, 3)
|
|
1546
|
+
continue
|
|
1547
|
+
|
|
1548
|
+
elif line.startswith('> ION') and line.endswith('> D1D2'):
|
|
1549
|
+
skip_lines(fh, 3)
|
|
1550
|
+
continue
|
|
1551
|
+
|
|
1552
|
+
elif line.startswith('> ION') and line.endswith('> CNVX'):
|
|
1553
|
+
skip_lines(fh, 3)
|
|
1554
|
+
continue
|
|
1555
|
+
|
|
1556
|
+
elif line.startswith('> ION') and line.endswith('> IFNV'):
|
|
1557
|
+
skip_lines(fh, 2)
|
|
1558
|
+
continue
|
|
1559
|
+
|
|
1560
|
+
if not line.startswith('> EPH'):
|
|
1561
|
+
continue
|
|
1562
|
+
|
|
1563
|
+
fields = line.split()
|
|
1564
|
+
sat = fields[2]
|
|
1565
|
+
satellite = Satellite(sat[0], int(sat[1:])) # satellite from eph
|
|
1566
|
+
|
|
1567
|
+
eph_type = _get_eph_type_from_rinex4_nav_block_header(line)
|
|
1568
|
+
n_lines = Nav.RINEX4_EPH_BLOCK_LINES[eph_type]
|
|
1569
|
+
|
|
1570
|
+
lines = [fh.readline().rstrip() for _ in range(n_lines)]
|
|
1571
|
+
|
|
1572
|
+
epoch_line = lines[0]
|
|
1573
|
+
epoch = datetime.datetime.strptime(epoch_line[4:23], "%Y %m %d %H %M %S")
|
|
1574
|
+
|
|
1575
|
+
block_lines = [line] + lines
|
|
1576
|
+
block = RawNavBlock(satellite, epoch, block_lines)
|
|
1577
|
+
|
|
1578
|
+
blocks.append(block)
|
|
1579
|
+
|
|
1580
|
+
return blocks
|
|
1581
|
+
|
|
1582
|
+
def get(satellite: Satellite):
|
|
1583
|
+
pass
|
|
1584
|
+
|
|
1585
|
+
def to_blocks(self) -> Dict[EphType, List[NavBlock]]:
|
|
1586
|
+
|
|
1587
|
+
out = {}
|
|
1588
|
+
|
|
1589
|
+
for block in self.blocks:
|
|
1590
|
+
|
|
1591
|
+
try:
|
|
1592
|
+
nav_block = block.to_nav_block()
|
|
1593
|
+
except ValueError:
|
|
1594
|
+
pass
|
|
1595
|
+
|
|
1596
|
+
eph_type = nav_block.get_type()
|
|
1597
|
+
|
|
1598
|
+
if eph_type not in out:
|
|
1599
|
+
out[eph_type] = []
|
|
1600
|
+
|
|
1601
|
+
out[eph_type].append(nav_block)
|
|
1602
|
+
|
|
1603
|
+
return out
|
|
1604
|
+
|
|
1605
|
+
def to_dataframes(self) -> Dict[EphType, pd.DataFrame]:
|
|
1606
|
+
|
|
1607
|
+
blocks = self.to_blocks()
|
|
1608
|
+
|
|
1609
|
+
dfs = {}
|
|
1610
|
+
|
|
1611
|
+
for eph_type, nav_blocks in blocks.items():
|
|
1612
|
+
data = [block.to_dict() for block in nav_blocks]
|
|
1613
|
+
dfs[eph_type] = pd.DataFrame(data)
|
|
1614
|
+
|
|
1615
|
+
return dfs
|
|
1616
|
+
|
|
1617
|
+
def __len__(self):
|
|
1618
|
+
"""
|
|
1619
|
+
Number of blocks of the Rinex Nav file
|
|
1620
|
+
"""
|
|
1621
|
+
return len(self.blocks)
|
|
1622
|
+
|
|
1623
|
+
def __iter__(self):
|
|
1624
|
+
"""
|
|
1625
|
+
Iterator for the Rinex blocks, to be used in for loops
|
|
1626
|
+
"""
|
|
1627
|
+
return iter(self.blocks)
|
|
1628
|
+
|
|
1629
|
+
def __lt__(self, other):
|
|
1630
|
+
return self.blocks < other.blocks
|
|
1631
|
+
|
|
1632
|
+
@staticmethod
|
|
1633
|
+
def create_header(pgm: str = "pygnss") -> str:
|
|
1634
|
+
"""
|
|
1635
|
+
Create a basic RINEX 4.99 header
|
|
1636
|
+
|
|
1637
|
+
Subversion 99 stands for Rokubun implementation of Rinex 4 standard
|
|
1638
|
+
that supports LEO navigation blocks
|
|
1639
|
+
"""
|
|
1640
|
+
|
|
1641
|
+
epoch_str = datetime.datetime.now(datetime.UTC).strftime('%Y%m%d %H%M%S UTC ')
|
|
1642
|
+
|
|
1643
|
+
out = " 4.99 NAVIGATION DATA M RINEX VERSION / TYPE\n"
|
|
1644
|
+
out = out + f"{pgm.ljust(20)}rokubun {epoch_str}PGM / RUN BY / DATE\n"
|
|
1645
|
+
out = out + " 18 LEAP SECONDS\n"
|
|
1646
|
+
out = out + " END OF HEADER\n"
|
|
1647
|
+
return out
|
|
1648
|
+
|
|
1649
|
+
@staticmethod
|
|
1650
|
+
def create_navblock(satellite: Satellite, orbit: Kepler, sat_clock: Clock, code_biases: CodeBiases) -> RawNavBlock:
|
|
1651
|
+
|
|
1652
|
+
WRITERS = {
|
|
1653
|
+
ConstellationId.STARLINK: Nav.create_leo_navblock,
|
|
1654
|
+
ConstellationId.SPIRE: Nav.create_leo_navblock,
|
|
1655
|
+
ConstellationId.LEO: Nav.create_leo_navblock,
|
|
1656
|
+
ConstellationId.ONEWEB: Nav.create_leo_navblock
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
writer = WRITERS.get(satellite.constellation, None)
|
|
1660
|
+
if writer:
|
|
1661
|
+
return writer(satellite, orbit, sat_clock, code_biases)
|
|
1662
|
+
|
|
1663
|
+
@staticmethod
|
|
1664
|
+
def create_leo_navblock(satellite: Satellite, orbit: Kepler, sat_clock: Clock, biases: CodeBiases) -> RawNavBlock:
|
|
1665
|
+
"""
|
|
1666
|
+
Output a RINEX4 navigation block from a set of orbit and clock parameters
|
|
1667
|
+
"""
|
|
1668
|
+
|
|
1669
|
+
lines = []
|
|
1670
|
+
|
|
1671
|
+
# Header line
|
|
1672
|
+
lines.append(f'> EPH {satellite.constellation.to_char()}{satellite.prn:05d}')
|
|
1673
|
+
|
|
1674
|
+
# EPOCH - CLK epoch
|
|
1675
|
+
lines.append(orbit.toe.strftime(' %Y %m %d %H %M %S') +
|
|
1676
|
+
f'{sat_clock.bias_s:19.12e}{sat_clock.drift_s_per_s:19.12e}{sat_clock.drift_rate_s_per_s2:19.12e}')
|
|
1677
|
+
|
|
1678
|
+
# ORBIT 1
|
|
1679
|
+
adot = 0.0
|
|
1680
|
+
crs = 0.0
|
|
1681
|
+
delta_n0 = 0.0
|
|
1682
|
+
M0 = orbit.true_anomaly_rad
|
|
1683
|
+
lines.append(Nav._write_orbit_line(adot, crs, delta_n0, M0))
|
|
1684
|
+
|
|
1685
|
+
# ORBIT 2
|
|
1686
|
+
cuc = 0.0
|
|
1687
|
+
e = orbit.eccentricity
|
|
1688
|
+
cus = 0.0
|
|
1689
|
+
sqrta = math.sqrt(orbit.a_m)
|
|
1690
|
+
lines.append(Nav._write_orbit_line(cuc, e, cus, sqrta))
|
|
1691
|
+
|
|
1692
|
+
# ORBIT - 3
|
|
1693
|
+
weektow = time.to_week_tow(orbit.toe)
|
|
1694
|
+
void = 0
|
|
1695
|
+
cic = 0.0
|
|
1696
|
+
# The RAAN parameter of GPS orbits are referred to the start of the GPS week, not the epoch
|
|
1697
|
+
# epoch of the ephemeris. Therfore, we need to compensate it
|
|
1698
|
+
t_start_of_week = time.from_week_tow(weektow.week, 0.0)
|
|
1699
|
+
greenwich_raan_rad = compute_greenwich_ascending_node_rad(t_start_of_week)
|
|
1700
|
+
OMEGA0 = orbit.raan_rad - greenwich_raan_rad
|
|
1701
|
+
OMEGA0 = math.fmod(OMEGA0 + math.tau, math.tau)
|
|
1702
|
+
|
|
1703
|
+
cis = 0.0
|
|
1704
|
+
lines.append(Nav._write_orbit_line(weektow.tow, cic, OMEGA0, cis))
|
|
1705
|
+
|
|
1706
|
+
# ORBIT - 4
|
|
1707
|
+
i0 = orbit.inclination_rad
|
|
1708
|
+
crc = 0.0
|
|
1709
|
+
omega = orbit.arg_perigee_rad
|
|
1710
|
+
OMEGA_DOT = 0.0
|
|
1711
|
+
lines.append(Nav._write_orbit_line(i0, crc, omega, OMEGA_DOT))
|
|
1712
|
+
|
|
1713
|
+
# ORBIT - 5
|
|
1714
|
+
void = 0.0
|
|
1715
|
+
idot = 0.0
|
|
1716
|
+
delta_n_dot = orbit.delta_n_dot_rad_per_s
|
|
1717
|
+
lines.append(Nav._write_orbit_line(idot, delta_n_dot, weektow.week, void))
|
|
1718
|
+
|
|
1719
|
+
# ORBIT - 6
|
|
1720
|
+
lines.append(Nav._write_orbit_line(void, void, biases.get_base_tgd(), void))
|
|
1721
|
+
|
|
1722
|
+
# ORBIT - 7
|
|
1723
|
+
channel = TrackingChannel.from_string('9C') # May depend on the LEO constellation
|
|
1724
|
+
lines.append(Nav._write_orbit_line(biases.get_code_bias(channel), void, void, void))
|
|
1725
|
+
|
|
1726
|
+
return RawNavBlock(satellite, orbit.toe, lines)
|
|
1727
|
+
|
|
1728
|
+
@staticmethod
|
|
1729
|
+
@process_filename_or_file_handler('w')
|
|
1730
|
+
def write_from_tle(output, tle_list: List[TLE], rinex2=False, sat_clock_model=ZeroClockModel(), code_biases=ZeroCodeBiases()) -> None:
|
|
1731
|
+
|
|
1732
|
+
output.write(Nav.create_header(pgm='rinex_from_file'))
|
|
1733
|
+
|
|
1734
|
+
for tle in tle_list:
|
|
1735
|
+
|
|
1736
|
+
try:
|
|
1737
|
+
satellite = tle.get_satellite()
|
|
1738
|
+
orbit = tle.to_kepler()
|
|
1739
|
+
sat_clock = sat_clock_model.get_clock(satellite, tle.toe)
|
|
1740
|
+
nav_block = Nav.create_navblock(satellite, orbit, sat_clock, code_biases)
|
|
1741
|
+
|
|
1742
|
+
output.write(nav_block.to_rinex2() if rinex2 else str(nav_block))
|
|
1743
|
+
output.write('\n')
|
|
1744
|
+
except ValueError as e:
|
|
1745
|
+
logger.warning(e)
|
|
1746
|
+
continue
|
|
1747
|
+
|
|
1748
|
+
@staticmethod
|
|
1749
|
+
@process_filename_or_file_handler('w')
|
|
1750
|
+
def write_from_dataframe(output, df: pd.DataFrame, rinex2=False, sat_clock_model=ZeroClockModel(), clock_biases=ZeroCodeBiases()) -> None:
|
|
1751
|
+
f"""
|
|
1752
|
+
Write a RINEX file from the orbital parameters contained in a DataFrame,
|
|
1753
|
+
which should have the following fields
|
|
1754
|
+
- {EPOCH_STR}, with the reference epoch (will be parsed to a datetime)
|
|
1755
|
+
- {SAT_STR}, with the satellite identifier
|
|
1756
|
+
- {A_M_STR}, with the semimajor axis
|
|
1757
|
+
- {ECCENTRICITY_STR} Eccentricity of the orbit (adimensional)
|
|
1758
|
+
- {INCLINATION_DEG_STR} Inclination of the orbit (degrees)
|
|
1759
|
+
- {RIGHT_ASCENSION_DEG_STR} Right ascension of the ascending node (degrees)
|
|
1760
|
+
- {ARG_PERIGEE_DEG_STR} Argument of the perigee (degrees)
|
|
1761
|
+
- {TRUE_ANOMALY_DEG_STR} True anomaly (degrees)
|
|
1762
|
+
|
|
1763
|
+
"""
|
|
1764
|
+
|
|
1765
|
+
output.write(Nav.create_header(pgm='rinex_from_file'))
|
|
1766
|
+
|
|
1767
|
+
df = df.sort_values(by=['epoch', 'sat'], ascending=[True, True])
|
|
1768
|
+
|
|
1769
|
+
for _, row in df.iterrows():
|
|
1770
|
+
|
|
1771
|
+
try:
|
|
1772
|
+
constellation = ConstellationId.from_string(row.sat[0])
|
|
1773
|
+
prn = int(row.sat[1:])
|
|
1774
|
+
satellite = Satellite(constellation=constellation, prn=prn)
|
|
1775
|
+
sat_clock = sat_clock_model.get_clock(satellite, row.epoch)
|
|
1776
|
+
orbit = Kepler(
|
|
1777
|
+
row.epoch, row.a_m, row.eccentricity,
|
|
1778
|
+
math.radians(row.inclination_deg), math.radians(row.raan_deg),
|
|
1779
|
+
math.radians(row.arg_perigee_deg), math.radians(row.true_anomaly_deg))
|
|
1780
|
+
nav_block = Nav.create_navblock(satellite, orbit, sat_clock, clock_biases)
|
|
1781
|
+
|
|
1782
|
+
output.write(nav_block.to_rinex2() if rinex2 else str(nav_block))
|
|
1783
|
+
output.write('\n')
|
|
1784
|
+
except Exception as e:
|
|
1785
|
+
logger.warning(e)
|
|
1786
|
+
continue
|
|
1787
|
+
|
|
1788
|
+
def write(self, output_filename, rinex2=False) -> None:
|
|
1789
|
+
|
|
1790
|
+
with open(output_filename, 'w') as fh:
|
|
1791
|
+
|
|
1792
|
+
fh.write(Nav.create_header(pgm='rinex_from_file'))
|
|
1793
|
+
|
|
1794
|
+
for block in self.blocks:
|
|
1795
|
+
try:
|
|
1796
|
+
fh.write(block.to_rinex2() if rinex2 else str(block))
|
|
1797
|
+
fh.write('\n')
|
|
1798
|
+
except Exception as e:
|
|
1799
|
+
logger.warning(e)
|
|
1800
|
+
continue
|
|
1801
|
+
|
|
1802
|
+
# def to_csv(self, csv_filename:str) -> None:
|
|
1803
|
+
#
|
|
1804
|
+
# with open(csv_filename, "w") as fh:
|
|
1805
|
+
#
|
|
1806
|
+
# for block in self.blocks:
|
|
1807
|
+
# pass
|
|
1808
|
+
|
|
1809
|
+
@staticmethod
|
|
1810
|
+
def _write_orbit_line(a: float, b: float, c: float, d: float) -> str:
|
|
1811
|
+
return f' {a:19.12e}{b:19.12e}{c:19.12e}{d:19.12e}'
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def compute_greenwich_ascending_node_rad(epoch_utc: datetime.datetime) -> float:
|
|
1815
|
+
"""
|
|
1816
|
+
Compute the Ascending Node of the Greenwich meridian at the given UTC epoch
|
|
1817
|
+
|
|
1818
|
+
>>> round(compute_greenwich_ascending_node_rad(datetime.datetime(2024, 2, 11)), 9)
|
|
1819
|
+
2.453307616
|
|
1820
|
+
"""
|
|
1821
|
+
|
|
1822
|
+
gmst_h = time.gmst(epoch_utc)
|
|
1823
|
+
|
|
1824
|
+
gmst_rad = gmst_h * math.tau / 24.0
|
|
1825
|
+
gmst_rad = gmst_rad % math.tau
|
|
1826
|
+
|
|
1827
|
+
return gmst_rad
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def merge_nav(files: List[Union[str, IO]]) -> str:
|
|
1831
|
+
"""
|
|
1832
|
+
Merge RINEX navigation files
|
|
1833
|
+
|
|
1834
|
+
:param files: list of RINEX Nav files to merge
|
|
1835
|
+
|
|
1836
|
+
:return: the merged RINEX NAV file as a string
|
|
1837
|
+
"""
|
|
1838
|
+
|
|
1839
|
+
# Read navigation blocks and place them into memory
|
|
1840
|
+
rinex_navs = [Nav(file) for file in files]
|
|
1841
|
+
|
|
1842
|
+
sorted_blocks = sorted([block for rinex_nav in rinex_navs for block in rinex_nav])
|
|
1843
|
+
|
|
1844
|
+
# Proceed to output all of them
|
|
1845
|
+
out = Nav.create_header(pgm="merge_rinex_nav")
|
|
1846
|
+
|
|
1847
|
+
out += '\n'.join([str(block) for block in sorted_blocks])
|
|
1848
|
+
|
|
1849
|
+
return out
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def merge_nav_cli():
|
|
1853
|
+
|
|
1854
|
+
parser = argparse.ArgumentParser(description="Tool to merge various RINEX 4 files",
|
|
1855
|
+
formatter_class=argparse.RawDescriptionHelpFormatter) # for verbatim
|
|
1856
|
+
|
|
1857
|
+
parser.add_argument('files', metavar='FILE', type=str, nargs='+', help='input RINEX4 file(s)')
|
|
1858
|
+
|
|
1859
|
+
args = parser.parse_args()
|
|
1860
|
+
|
|
1861
|
+
merged_rinex_str = merge_nav(args.files)
|
|
1862
|
+
|
|
1863
|
+
sys.stdout.write(merged_rinex_str)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def rinex_from_file():
|
|
1867
|
+
|
|
1868
|
+
parser = argparse.ArgumentParser(description="Tool to convert an input file to RINEX navigation file",
|
|
1869
|
+
formatter_class=argparse.RawDescriptionHelpFormatter) # for verbatim
|
|
1870
|
+
|
|
1871
|
+
# Define the mutually exclusive group
|
|
1872
|
+
input_options = parser.add_mutually_exclusive_group(required=True)
|
|
1873
|
+
|
|
1874
|
+
input_options.add_argument('--celestrak_file', metavar='<filename>', type=str,
|
|
1875
|
+
help='File from Celestrak with the TLE elements to convert to RINEX.' +
|
|
1876
|
+
'Based on https://arxiv.org/abs/2401.17767')
|
|
1877
|
+
|
|
1878
|
+
input_options.add_argument('--csv', metavar='<filename>', type=str,
|
|
1879
|
+
help='CSV file with the description of ')
|
|
1880
|
+
|
|
1881
|
+
parser.add_argument('--clock-model', '-c', choices=[ZERO_STR, RANDOM_STR], required=False,
|
|
1882
|
+
help="Choose the clock model to be applied to satellites where clock \
|
|
1883
|
+
bias and drift has not been provided. Defaults to '{ZERO_STR}'")
|
|
1884
|
+
|
|
1885
|
+
parser.add_argument('--rinex2', action='store_true',
|
|
1886
|
+
help='Output the format in Rinex 2 GPS format. Will skip satellites \
|
|
1887
|
+
with PRN larger than 99')
|
|
1888
|
+
|
|
1889
|
+
# Parse the command-line arguments
|
|
1890
|
+
args = parser.parse_args()
|
|
1891
|
+
|
|
1892
|
+
sat_clock_model = ZeroClockModel()
|
|
1893
|
+
if args.clock_model == RANDOM_STR:
|
|
1894
|
+
sat_clock_model = GnssRandomClock()
|
|
1895
|
+
|
|
1896
|
+
code_biases = ZeroCodeBiases()
|
|
1897
|
+
if args.code_biases == RANDOM_STR:
|
|
1898
|
+
TGD_MAX_S = 10.0e-9
|
|
1899
|
+
tgd_s = np.random.uniform(low=-TGD_MAX_S, high=TGD_MAX_S)
|
|
1900
|
+
isc_s9c_s = np.random.uniform(low=-TGD_MAX_S, high=TGD_MAX_S)
|
|
1901
|
+
code_biases = LEOCodeBiases(tgd_s, isc_s9c_s)
|
|
1902
|
+
|
|
1903
|
+
if args.celestrak_file:
|
|
1904
|
+
tle_list = read_celestrak(args.celestrak_file)
|
|
1905
|
+
Nav.write_from_tle(sys.stdout, tle_list, rinex2=args.rinex2, sat_clock_model=sat_clock_model, code_biases=code_biases)
|
|
1906
|
+
|
|
1907
|
+
elif args.csv:
|
|
1908
|
+
df = pd.read_csv(args.csv, parse_dates=['epoch'])
|
|
1909
|
+
Nav.write_from_dataframe(sys.stdout, df, rinex2=args.rinex2, sat_clock_model=sat_clock_model, code_biases=code_biases)
|
|
1910
|
+
|
|
1911
|
+
def to_parquet(rinex_files:List[str], output_filename:str, station_name:str = None):
|
|
1912
|
+
"""
|
|
1913
|
+
Convert a list of RINEX files into a parquet file
|
|
1914
|
+
"""
|
|
1915
|
+
|
|
1916
|
+
if output_filename is None:
|
|
1917
|
+
output_filename = rinex_files[0] + '.parquet'
|
|
1918
|
+
|
|
1919
|
+
dfs = []
|
|
1920
|
+
|
|
1921
|
+
for rinex_file in rinex_files:
|
|
1922
|
+
df = to_dataframe(rinex_file, station_name=station_name)
|
|
1923
|
+
dfs.append(df)
|
|
1924
|
+
|
|
1925
|
+
df = pd.concat(dfs)
|
|
1926
|
+
|
|
1927
|
+
df.to_parquet(output_filename)
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
def rinex_to_parquet():
|
|
1931
|
+
|
|
1932
|
+
parser = argparse.ArgumentParser(description="Tool to convert an observable RINEX file to parquet file",
|
|
1933
|
+
formatter_class=argparse.RawDescriptionHelpFormatter) # for verbatim
|
|
1934
|
+
|
|
1935
|
+
parser.add_argument('rinex_files', metavar='N', type=str, nargs='+',
|
|
1936
|
+
help='rinex file(s) to convert to a single parquet file')
|
|
1937
|
+
|
|
1938
|
+
parser.add_argument('-o', '--output', type=str, help="output parquet file name. If not provided, the first file ended with the 'parquet' extension will be used")
|
|
1939
|
+
parser.add_argument('-n', '--name', type=str, help="Station name to be included in the parquet file. If not provided, will nbe inferred from the RINEX file")
|
|
1940
|
+
|
|
1941
|
+
# Parse the command-line arguments
|
|
1942
|
+
args = parser.parse_args()
|
|
1943
|
+
|
|
1944
|
+
to_parquet(args.rinex_files, output_filename=args.output, station_name=args.name)
|
|
1945
|
+
|
|
1946
|
+
_ACCURACY_STR = "accuracy"
|
|
1947
|
+
_ADOT_STR = "Adot[m/s]"
|
|
1948
|
+
_AODC_STR = "aodc"
|
|
1949
|
+
_AODE_STR = "aode"
|
|
1950
|
+
_B1C_INTEGRITY_FLAGS_STR = "b1c_integrity_flags"
|
|
1951
|
+
_B2A_INTEGRITY_FLAGS_STR = "b2a_integrity_flags"
|
|
1952
|
+
_B2B_INTEGRITY_FLAGS_STR = "b2b_integrity_flags"
|
|
1953
|
+
_BGD_E5A_STR = "bgd_e5a_e1[s]"
|
|
1954
|
+
_BGD_E5B_STR = "bgd_e5b_e1[s]"
|
|
1955
|
+
_CIC_STR = "cic[rad]"
|
|
1956
|
+
_CIS_STR = "cis[rad]"
|
|
1957
|
+
_CLK_BIAS_STR = "clock_bias[s]"
|
|
1958
|
+
_CLK_DRIFT_RATE_STR = "clock_drift_rate[s/s2]"
|
|
1959
|
+
_CLK_DRIFT_STR = "clock_drift[s/s]"
|
|
1960
|
+
_CODESL2_STR = "codesL2"
|
|
1961
|
+
_CRC_STR = "crc[m]"
|
|
1962
|
+
_CRS_STR = "crs[m]"
|
|
1963
|
+
_CUC_STR = "cuc[rad]"
|
|
1964
|
+
_CUS_STR = "cus[rad]"
|
|
1965
|
+
_DATA_SOURCES_STR = "datasources"
|
|
1966
|
+
_DELTAN_DOT_STR = "deltan_dot[r/s^2]"
|
|
1967
|
+
_DELTAN_STR = "deltan[rad/s]"
|
|
1968
|
+
_EPOCH_STR = "epoch"
|
|
1969
|
+
_E_STR = "e"
|
|
1970
|
+
_FIT_INTERVAL_STR = "fit_interval"
|
|
1971
|
+
_HEALTH_STR = "health"
|
|
1972
|
+
_I0_STR = "i0[rad]"
|
|
1973
|
+
_IDOT_STR = "idot[rad/s]"
|
|
1974
|
+
_IODC_STR = "iodc"
|
|
1975
|
+
_IODE_STR = "iode"
|
|
1976
|
+
_IODNAV_STR = "iodnav"
|
|
1977
|
+
_ISC_B2AD_STR = "isc_b2ad[s]"
|
|
1978
|
+
_ISC_B1CD_STR = "isc_b1cd[s]"
|
|
1979
|
+
_ISC_L1CA_STR = "isc_L1CA[s]"
|
|
1980
|
+
_ISC_L1CD_STR = "isc_L1CD[s]"
|
|
1981
|
+
_ISC_L1CP_STR = "isc_L1CP[s]"
|
|
1982
|
+
_ISC_L2C_STR = "isc_L2C[s]"
|
|
1983
|
+
_ISC_L5I5_STR = "isc_L5I5[s]"
|
|
1984
|
+
_ISC_L5Q5_STR = "isc_L5Q5[s]"
|
|
1985
|
+
_L2PFLAG_STR = "l2p_flag"
|
|
1986
|
+
_M0_STR = "M0[rad]"
|
|
1987
|
+
_OMEGA0_STR = "OMEGA0[rad]"
|
|
1988
|
+
_OMEGA_DOT_STR = "OMEGA_DOT[rad/s]"
|
|
1989
|
+
_OMEGA_STR = "omega[rad]"
|
|
1990
|
+
_SATH1_STR = "sat_H1"
|
|
1991
|
+
_SAT_STR = "sat"
|
|
1992
|
+
_SAT_TYPE_STR = "sat_type"
|
|
1993
|
+
_SISAI_OC1_STR = "SISAI_oc1"
|
|
1994
|
+
_SISAI_OC2_STR = "SISAI_oc2"
|
|
1995
|
+
_SISAI_OCB_STR = "SISAI_ocb"
|
|
1996
|
+
_SISAI_OE_STR = "SISAI_oe"
|
|
1997
|
+
_SISA_STR = "sisa[m]"
|
|
1998
|
+
_SISMAI_STR = "SISMAI"
|
|
1999
|
+
_SQRTA_STR = "sqrtA[sqrt(m)]"
|
|
2000
|
+
_TGD1_STR = "tgd1[s]"
|
|
2001
|
+
_TGD2_STR = "tgd2[s]"
|
|
2002
|
+
_TGD_B1CP = "tgd_b1cp[s]"
|
|
2003
|
+
_TGD_B2AP = "tgd_b2ap[s]"
|
|
2004
|
+
_TGD_B2BI = "tgd_b2bi[s]"
|
|
2005
|
+
_TGD_STR = "tgd[s]"
|
|
2006
|
+
_TOE_BDT_TOW_STR = "toe_bdt_tow[s]"
|
|
2007
|
+
_TOE_BDT_WEEK_STR = "toe_bdt_week[week]"
|
|
2008
|
+
_TOE_GAL_TOW_STR = "toe_gal_tow[s]"
|
|
2009
|
+
_TOE_GAL_WEEK_STR = "toe_gal_week[week]"
|
|
2010
|
+
_TOE_SOW_STR = "toe[s]"
|
|
2011
|
+
_TOE_WEEK_STR = "toe[week]"
|
|
2012
|
+
_TX_TIME_TOW_STR = "tx_time[s]"
|
|
2013
|
+
_T_OP_STR = "t_op[s]"
|
|
2014
|
+
_URAI_ED_STR = "urai_ed"
|
|
2015
|
+
_URAI_NED0_STR = "urai_ned0"
|
|
2016
|
+
_URAI_NED1_STR = "urai_ned1"
|
|
2017
|
+
_URAI_NED2_STR = "urai_ned2"
|
|
2018
|
+
_WN_OP_STR = "wn_op[week]"
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
def _parse_nav_epoch_line(line: str) -> tuple:
|
|
2022
|
+
"""
|
|
2023
|
+
Parse a Rinex 4 Navigation line
|
|
2024
|
+
|
|
2025
|
+
>>> _parse_nav_epoch_line("G01 2024 02 13 11 22 33 1.693181693554e-04 1.477928890381e-12 1.000000000000e+00")
|
|
2026
|
+
('G01', 2024, 2, 13, 11, 22, 33, 0.0001693181693554, 1.477928890381e-12, 1.0)
|
|
2027
|
+
"""
|
|
2028
|
+
|
|
2029
|
+
return line[0:3], \
|
|
2030
|
+
int(line[4:8]), int(line[9:11]), int(line[12:14]), \
|
|
2031
|
+
int(line[15:17]), int(line[17:20]), int(line[20:23]), \
|
|
2032
|
+
float(line[23:42]), float(line[42:61]), float(line[61:])
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def _parse_nav_orb_line(line: str) -> tuple:
|
|
2036
|
+
"""
|
|
2037
|
+
Parse a Rinex 4 Navigation line (broadcast orbit line)
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
>>> _parse_nav_orb_line(' 1.700595021248e-06')
|
|
2041
|
+
(1.700595021248e-06, None, None, None)
|
|
2042
|
+
|
|
2043
|
+
>>> _parse_nav_orb_line(' 1.700595021248e-06 1.270996686071e-02')
|
|
2044
|
+
(1.700595021248e-06, 0.01270996686071, None, None)
|
|
2045
|
+
|
|
2046
|
+
>>> _parse_nav_orb_line(' 1.700595021248e-06 1.270996686071e-02 1.259148120880e-05')
|
|
2047
|
+
(1.700595021248e-06, 0.01270996686071, 1.25914812088e-05, None)
|
|
2048
|
+
|
|
2049
|
+
>>> _parse_nav_orb_line(' 1.700595021248e-06 1.270996686071e-02 1.259148120880e-05 5.154011671066e+03')
|
|
2050
|
+
(1.700595021248e-06, 0.01270996686071, 1.25914812088e-05, 5154.011671066)
|
|
2051
|
+
"""
|
|
2052
|
+
|
|
2053
|
+
try:
|
|
2054
|
+
v1 = float(line[4:23])
|
|
2055
|
+
except ValueError:
|
|
2056
|
+
v1 = None
|
|
2057
|
+
|
|
2058
|
+
try:
|
|
2059
|
+
v2 = float(line[23:42])
|
|
2060
|
+
except ValueError:
|
|
2061
|
+
v2 = None
|
|
2062
|
+
|
|
2063
|
+
try:
|
|
2064
|
+
v3 = float(line[42:61])
|
|
2065
|
+
except ValueError:
|
|
2066
|
+
v3 = None
|
|
2067
|
+
|
|
2068
|
+
try:
|
|
2069
|
+
v4 = float(line[61:])
|
|
2070
|
+
except ValueError:
|
|
2071
|
+
v4 = None
|
|
2072
|
+
|
|
2073
|
+
return v1, v2, v3, v4
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _parse_obs_line(line: str, n_obs: int) -> Tuple[Satellite, List[ObservableValue]]:
|
|
2077
|
+
"""
|
|
2078
|
+
|
|
2079
|
+
>>> line = "C05 40058862.469 6 208597044.05206 40058858.572 7 161300483.44407 40058861.947 7 169502210.29507"
|
|
2080
|
+
>>> _parse_obs_line(line, 6)
|
|
2081
|
+
(C05, [ObservableValue(value=40058862.469, lli=0, snr=6), \
|
|
2082
|
+
ObservableValue(value=208597044.052, lli=0, snr=6), \
|
|
2083
|
+
ObservableValue(value=40058858.572, lli=0, snr=7), \
|
|
2084
|
+
ObservableValue(value=161300483.444, lli=0, snr=7), \
|
|
2085
|
+
ObservableValue(value=40058861.947, lli=0, snr=7), \
|
|
2086
|
+
ObservableValue(value=169502210.295, lli=0, snr=7)])
|
|
2087
|
+
"""
|
|
2088
|
+
|
|
2089
|
+
satellite = Satellite.from_string(line[0:3])
|
|
2090
|
+
|
|
2091
|
+
observable_values = []
|
|
2092
|
+
|
|
2093
|
+
offset = 3
|
|
2094
|
+
for i_obs in range(n_obs):
|
|
2095
|
+
start = offset + i_obs * 16
|
|
2096
|
+
obs_str = line[start:start + 14]
|
|
2097
|
+
lli_str = line[start + 14:start + 14 + 1]
|
|
2098
|
+
snr_str = line[start + 15:start + 15 + 1]
|
|
2099
|
+
|
|
2100
|
+
obs = float(obs_str) if obs_str and obs_str != ' ' and obs_str != '\n' else math.nan
|
|
2101
|
+
lli = int(lli_str) if lli_str and lli_str != ' ' and lli_str != '\n' else 0
|
|
2102
|
+
snr = int(snr_str) if snr_str and snr_str != ' ' and snr_str != '\n' else 0
|
|
2103
|
+
|
|
2104
|
+
observable_values.append(ObservableValue(obs, lli, snr))
|
|
2105
|
+
|
|
2106
|
+
return satellite, observable_values
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def _parse_rnx3_epoch(line):
|
|
2110
|
+
"""
|
|
2111
|
+
Parse a measurement epoch from a Rinex3 and return a tuple
|
|
2112
|
+
with the epochm event type and number of lines
|
|
2113
|
+
|
|
2114
|
+
>>> _parse_rnx3_epoch("> 2017 08 03 11 22 30.1234000 0 29")
|
|
2115
|
+
(datetime.datetime(2017, 8, 3, 11, 22, 30, 123400), 0, 29)
|
|
2116
|
+
|
|
2117
|
+
>>> _parse_rnx3_epoch("> 2021 2 5 15 51 30.2000000 0 22")
|
|
2118
|
+
(datetime.datetime(2021, 2, 5, 15, 51, 30, 200000), 0, 22)
|
|
2119
|
+
|
|
2120
|
+
>>> _parse_rnx3_epoch("> 2020 11 18 21 43 30.0000000 0 28 0.000000000000")
|
|
2121
|
+
(datetime.datetime(2020, 11, 18, 21, 43, 30), 0, 28)
|
|
2122
|
+
|
|
2123
|
+
>>> _parse_rnx3_epoch("> 2019 07 02 13 25 5.9999995 0 31")
|
|
2124
|
+
(datetime.datetime(2019, 7, 2, 13, 25, 5, 999999), 0, 31)
|
|
2125
|
+
"""
|
|
2126
|
+
|
|
2127
|
+
try:
|
|
2128
|
+
_, year, month, day, hour, minute, seconds, epoch_flag, n_lines, *b = line.split()
|
|
2129
|
+
except ValueError as e:
|
|
2130
|
+
raise ValueError(f"Invalid Rinex 3 epoch line [ {line} ]: {e}")
|
|
2131
|
+
|
|
2132
|
+
seconds, microseconds, *b = seconds.split('.')
|
|
2133
|
+
|
|
2134
|
+
t = datetime.datetime(int(year), int(month), int(day),
|
|
2135
|
+
hour=int(hour), minute=int(minute), second=int(seconds),
|
|
2136
|
+
microsecond=int(microseconds[0:6]))
|
|
2137
|
+
|
|
2138
|
+
return t, int(epoch_flag), int(n_lines)
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _get_eph_type_from_rinex4_nav_block_header(block_header: str) -> EphType:
|
|
2142
|
+
"""
|
|
2143
|
+
Get the type of ephemeris based on the header line of the Rinex 4 navigation block
|
|
2144
|
+
|
|
2145
|
+
>>> _get_eph_type_from_rinex4_nav_block_header('> EPH G01 LNAV')
|
|
2146
|
+
<EphType.GPS_LNAV: 'G_LNAV'>
|
|
2147
|
+
|
|
2148
|
+
>>> _get_eph_type_from_rinex4_nav_block_header('> EPH X02434')
|
|
2149
|
+
<EphType.STARLINK: 'X'>
|
|
2150
|
+
"""
|
|
2151
|
+
|
|
2152
|
+
fields = block_header.split()
|
|
2153
|
+
|
|
2154
|
+
sat = Satellite.from_string(fields[2])
|
|
2155
|
+
|
|
2156
|
+
sat.constellation.value
|
|
2157
|
+
key = sat.constellation.value
|
|
2158
|
+
if len(fields) == 4:
|
|
2159
|
+
key = f'{key}_{fields[-1]}'
|
|
2160
|
+
|
|
2161
|
+
return EphType.from_string(key)
|