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/gnss/__init__.py
ADDED
|
File without changes
|
pygnss/gnss/edit.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
ARC_ID_FIELD = 'arc_id'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compute_phase_arc_id(data: pd.DataFrame) -> pd.DataFrame:
|
|
8
|
+
"""
|
|
9
|
+
Computes the phase arc ID, that can be used later to perform operations
|
|
10
|
+
on a per-arc basis (compute arc bias, ...)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
data[ARC_ID_FIELD] = data.groupby('signal')['slip'].transform('cumsum')
|
|
14
|
+
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mark_time_gap(data: pd.DataFrame, threshold_s: float = 5) -> pd.DataFrame:
|
|
19
|
+
"""
|
|
20
|
+
Mark a phase cycle slip when the time series show a time gap
|
|
21
|
+
of a given threshold
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Function to mark epochs with a difference > threshold
|
|
25
|
+
def mark_epochs(group):
|
|
26
|
+
EPOCH_FIELD = 'epoch'
|
|
27
|
+
marked = group[EPOCH_FIELD].diff() > pd.Timedelta(seconds=threshold_s)
|
|
28
|
+
return marked
|
|
29
|
+
|
|
30
|
+
# Apply the function per group using groupby
|
|
31
|
+
marked_epochs = data.groupby('signal').apply(mark_epochs)
|
|
32
|
+
|
|
33
|
+
data['slip'] = np.any([data['slip'], marked_epochs], axis=0)
|
|
34
|
+
|
|
35
|
+
data = compute_phase_arc_id(data)
|
|
36
|
+
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detrend(data: pd.DataFrame, observable: str, n_samples: int) -> pd.DataFrame:
|
|
41
|
+
"""
|
|
42
|
+
Detrend a given observable by using a rolling window of a certain number of
|
|
43
|
+
samples
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
trend_column = f'{observable}_trend'
|
|
47
|
+
detrended_column = f'{observable}_detrended'
|
|
48
|
+
|
|
49
|
+
trend = data.groupby(['signal', ARC_ID_FIELD])[observable].transform(lambda x: x.rolling(n_samples).mean())
|
|
50
|
+
data[trend_column] = trend
|
|
51
|
+
data[detrended_column] = data[observable] - data[trend_column]
|
|
52
|
+
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def remove_mean(data: pd.DataFrame, observable: str) -> pd.DataFrame:
|
|
57
|
+
|
|
58
|
+
bias_field = f'{observable}_bias'
|
|
59
|
+
aligned_field = f'{observable}_aligned'
|
|
60
|
+
|
|
61
|
+
# For each arch id, compute the median
|
|
62
|
+
data[bias_field] = data.groupby(['signal', ARC_ID_FIELD])[observable].transform('mean')
|
|
63
|
+
|
|
64
|
+
data[aligned_field] = data[observable] - data[bias_field]
|
|
65
|
+
|
|
66
|
+
return data
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from .types import ConstellationId, TrackingChannel
|
|
3
|
+
|
|
4
|
+
def compute_geometry_free(data: pd.DataFrame, constellation: ConstellationId, channel_a: TrackingChannel, channel_b: TrackingChannel) -> pd.DataFrame:
|
|
5
|
+
"""
|
|
6
|
+
Compute the geometry (ionospheric free combination)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
columns = ['epoch', 'constellation', 'sat', 'channel', 'signal', 'range', 'phase', 'slip']
|
|
10
|
+
|
|
11
|
+
# Create a new dataframe
|
|
12
|
+
df = data[columns].copy()
|
|
13
|
+
|
|
14
|
+
# Create subsets of the DataFrame corresponding to the constellation and each of
|
|
15
|
+
# the channels selected to build the ionospheric combination
|
|
16
|
+
df_a = df[(df['constellation'] == constellation.value) & (df['channel'] == str(channel_a))]
|
|
17
|
+
df_b = df[(df['constellation'] == constellation.value) & (df['channel'] == str(channel_b))]
|
|
18
|
+
|
|
19
|
+
# Compute the wavelength of the two tracking channels
|
|
20
|
+
wl_a = channel_a.get_wavelength(constellation)
|
|
21
|
+
wl_b = channel_b.get_wavelength(constellation)
|
|
22
|
+
|
|
23
|
+
# Use merge to join the two tables
|
|
24
|
+
df_out = pd.merge(df_a, df_b, on=['epoch', 'sat'], how='inner', suffixes=('_a', '_b'))
|
|
25
|
+
df_out['li_m'] = df_out['phase_a'] * wl_a - df_out['phase_b'] * wl_b
|
|
26
|
+
df_out['pi_m'] = df_out['range_b'] - df_out['range_a']
|
|
27
|
+
|
|
28
|
+
return df_out
|
|
29
|
+
|
|
30
|
+
def compute_code_minus_carrier(data: pd.DataFrame) -> pd.DataFrame:
|
|
31
|
+
"""
|
|
32
|
+
Compute the geometry (ionospheric free combination)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Create a new dataframe
|
|
36
|
+
df = data.copy()
|
|
37
|
+
|
|
38
|
+
# Compute the wavelength
|
|
39
|
+
df['wl'] = df.apply(lambda row : TrackingChannel.from_string(row['channel']).get_wavelength(row['constellation']), axis=1)
|
|
40
|
+
|
|
41
|
+
df['cmc'] = df['range'] - df['phase'] * df['wl']
|
|
42
|
+
|
|
43
|
+
return df
|
pygnss/gnss/residuals.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import List, Tuple, Dict
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
CONSTELLATION_FIELD_STR = 'constellation'
|
|
7
|
+
SAT_FIELD_STR = 'sat'
|
|
8
|
+
EPOCH_FIELD_STR = 'epoch'
|
|
9
|
+
AZIMUTH_FIELD_STR = 'az'
|
|
10
|
+
ELEVATION_FIELD_STR = 'el'
|
|
11
|
+
SNR_FIELD_STR = 'snr_dbHz'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Residuals(object):
|
|
15
|
+
|
|
16
|
+
def __init__(self, dataframe):
|
|
17
|
+
self.df = dataframe
|
|
18
|
+
|
|
19
|
+
def get_sat_visibility(self) -> Dict[str, List[Tuple[float, float]]]:
|
|
20
|
+
|
|
21
|
+
out = {}
|
|
22
|
+
|
|
23
|
+
for sat, group in self.df.groupby(SAT_FIELD_STR):
|
|
24
|
+
# FIXME this should take into account channel to avoid overwriting the info from different channels
|
|
25
|
+
out[sat] = list(zip(group[AZIMUTH_FIELD_STR], group[ELEVATION_FIELD_STR], group[SNR_FIELD_STR]))
|
|
26
|
+
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
def get_constellation_count(self) -> List[Tuple[datetime.datetime, dict]]:
|
|
30
|
+
|
|
31
|
+
n_sats = []
|
|
32
|
+
|
|
33
|
+
for epoch, epoch_group in self.df.groupby(EPOCH_FIELD_STR):
|
|
34
|
+
n_sats_per_epoch = {}
|
|
35
|
+
for constellation, df in epoch_group.groupby(CONSTELLATION_FIELD_STR):
|
|
36
|
+
n_sats_per_epoch[constellation] = len(pd.unique(df[SAT_FIELD_STR]))
|
|
37
|
+
|
|
38
|
+
n_sats.append((epoch.to_pydatetime(), n_sats_per_epoch))
|
|
39
|
+
|
|
40
|
+
return n_sats
|
|
41
|
+
|
|
42
|
+
def __len__(self):
|
|
43
|
+
return len(self.df)
|
pygnss/gnss/types.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
|
|
5
|
+
from pygnss.constants import SPEED_OF_LIGHT
|
|
6
|
+
from pygnss.decorator import deprecated
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConstellationId(Enum):
|
|
10
|
+
UNKNOWN: str = '0'
|
|
11
|
+
GPS: str = 'G'
|
|
12
|
+
GALILEO: str = 'E'
|
|
13
|
+
GLONASS: str = 'R'
|
|
14
|
+
BEIDOU: str = 'C'
|
|
15
|
+
QZSS: str = 'J'
|
|
16
|
+
IRNSS: str = 'I'
|
|
17
|
+
SBAS: str = 'S'
|
|
18
|
+
LEO: str = 'L'
|
|
19
|
+
XONA: str = 'P'
|
|
20
|
+
GEELY: str = 'Y'
|
|
21
|
+
STARLINK: str = 'X'
|
|
22
|
+
ONEWEB: str = 'O'
|
|
23
|
+
SPIRE: str = 'V'
|
|
24
|
+
CENTISPACE: str = 'Z'
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_string(constellation_char: str) -> 'ConstellationId':
|
|
28
|
+
"""
|
|
29
|
+
Returns the corresponding ConstellationId from input character
|
|
30
|
+
|
|
31
|
+
>>> ConstellationId.from_string('E')
|
|
32
|
+
<ConstellationId.GALILEO: 'E'>
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return ConstellationId(constellation_char)
|
|
36
|
+
except ValueError:
|
|
37
|
+
raise ValueError(f"Invalid constellation character: {constellation_char}")
|
|
38
|
+
|
|
39
|
+
def to_char(self):
|
|
40
|
+
"""
|
|
41
|
+
Constellation representation as character
|
|
42
|
+
|
|
43
|
+
>>> c = ConstellationId.GPS
|
|
44
|
+
>>> c.to_char()
|
|
45
|
+
'G'
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
return self.value
|
|
49
|
+
|
|
50
|
+
def __lt__(self, other):
|
|
51
|
+
"""
|
|
52
|
+
Less operator for the ConstellationId class
|
|
53
|
+
|
|
54
|
+
>>> c1 = ConstellationId.GPS
|
|
55
|
+
>>> c2 = ConstellationId.IRNSS
|
|
56
|
+
>>> c1 < c2
|
|
57
|
+
True
|
|
58
|
+
>>> c2 < c1
|
|
59
|
+
False
|
|
60
|
+
>>> c2 < 0
|
|
61
|
+
Traceback (most recent call last):
|
|
62
|
+
...
|
|
63
|
+
TypeError: '<' not supported between instances of 'ConstellationId' and 'int'
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if isinstance(other, ConstellationId):
|
|
67
|
+
return self.value < other.value
|
|
68
|
+
return NotImplemented
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Band:
|
|
72
|
+
"""
|
|
73
|
+
Namespace to define the supported bands (center frequency in Hz)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
L1 = 1575420000
|
|
77
|
+
L2 = 1227600000
|
|
78
|
+
L5 = 1176450000
|
|
79
|
+
L6 = 1278750000
|
|
80
|
+
G1 = 1602000000
|
|
81
|
+
G1a = 1600995000
|
|
82
|
+
G2 = 1246000000
|
|
83
|
+
G2a = 1248060000
|
|
84
|
+
G3 = 1202025000
|
|
85
|
+
E1 = 1575420000
|
|
86
|
+
E5a = 1176450000
|
|
87
|
+
E5b = 1207140000
|
|
88
|
+
E5 = 1191795000
|
|
89
|
+
E6 = 1278750000
|
|
90
|
+
B1_2 = 1561098000
|
|
91
|
+
B1 = 1575420000
|
|
92
|
+
B2a = 1176450000
|
|
93
|
+
B2b = 1207140000
|
|
94
|
+
B2 = 1191795000
|
|
95
|
+
B3 = 1268520000
|
|
96
|
+
S = 2492028000
|
|
97
|
+
|
|
98
|
+
def __init__(self):
|
|
99
|
+
"""
|
|
100
|
+
Init the class (does nothing)
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def get_freq_from_rinex_band(constellation: Union[ConstellationId, str], rinex_band: int) -> int:
|
|
106
|
+
"""
|
|
107
|
+
Get the band from the RINEX band number (and constellation)
|
|
108
|
+
|
|
109
|
+
>>> Band.get_freq_from_rinex_band(ConstellationId.GPS, 1)
|
|
110
|
+
1575420000
|
|
111
|
+
>>> Band.get_freq_from_rinex_band('G', 1)
|
|
112
|
+
1575420000
|
|
113
|
+
>>> Band.get_freq_from_rinex_band(ConstellationId.GALILEO, 5)
|
|
114
|
+
1176450000
|
|
115
|
+
>>> Band.get_freq_from_rinex_band('E', 5)
|
|
116
|
+
1176450000
|
|
117
|
+
>>> Band.get_freq_from_rinex_band(ConstellationId.GPS, 8)
|
|
118
|
+
Traceback (most recent call last):
|
|
119
|
+
...
|
|
120
|
+
ValueError: Invalid band 8 for constellation ConstellationId.GPS
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
band = None
|
|
124
|
+
|
|
125
|
+
if isinstance(constellation, ConstellationId) == True:
|
|
126
|
+
constellation = constellation.value
|
|
127
|
+
|
|
128
|
+
if constellation == ConstellationId.GPS.value:
|
|
129
|
+
if rinex_band == 1:
|
|
130
|
+
band = Band.L1
|
|
131
|
+
elif rinex_band == 2:
|
|
132
|
+
band = Band.L2
|
|
133
|
+
elif rinex_band == 5:
|
|
134
|
+
band = Band.L5
|
|
135
|
+
elif rinex_band == 6:
|
|
136
|
+
band = Band.L6
|
|
137
|
+
|
|
138
|
+
elif constellation == ConstellationId.GLONASS.value:
|
|
139
|
+
if rinex_band == 1:
|
|
140
|
+
band = Band.G1
|
|
141
|
+
elif rinex_band == 4:
|
|
142
|
+
band = Band.G1a
|
|
143
|
+
elif rinex_band == 2:
|
|
144
|
+
band = Band.G2
|
|
145
|
+
elif rinex_band == 6:
|
|
146
|
+
band = Band.G2a
|
|
147
|
+
elif rinex_band == 3:
|
|
148
|
+
band = Band.G3
|
|
149
|
+
|
|
150
|
+
elif constellation == ConstellationId.GALILEO.value:
|
|
151
|
+
if rinex_band == 1:
|
|
152
|
+
band = Band.E1
|
|
153
|
+
elif rinex_band == 5:
|
|
154
|
+
band = Band.E5a
|
|
155
|
+
elif rinex_band == 7:
|
|
156
|
+
band = Band.E5b
|
|
157
|
+
elif rinex_band == 8:
|
|
158
|
+
band = Band.E5
|
|
159
|
+
elif rinex_band == 6:
|
|
160
|
+
band = Band.E6
|
|
161
|
+
|
|
162
|
+
elif constellation == ConstellationId.BEIDOU.value:
|
|
163
|
+
if rinex_band == 2:
|
|
164
|
+
band = Band.B1_2
|
|
165
|
+
elif rinex_band == 1:
|
|
166
|
+
band = Band.B1 = 1575420000
|
|
167
|
+
elif rinex_band == 5:
|
|
168
|
+
band = Band.B2a = 1176450000
|
|
169
|
+
elif rinex_band == 7:
|
|
170
|
+
band = Band.B2b = 1207140000
|
|
171
|
+
elif rinex_band == 8:
|
|
172
|
+
band = Band.B2 = 1191795000
|
|
173
|
+
elif rinex_band == 6:
|
|
174
|
+
band = Band.B3 = 1268520000
|
|
175
|
+
|
|
176
|
+
elif constellation == ConstellationId.IRNSS.value:
|
|
177
|
+
if rinex_band == 5:
|
|
178
|
+
band = Band.L5
|
|
179
|
+
elif rinex_band == 9:
|
|
180
|
+
band = Band.S
|
|
181
|
+
|
|
182
|
+
elif constellation == ConstellationId.SBAS.value:
|
|
183
|
+
if rinex_band == 1:
|
|
184
|
+
band = Band.L1
|
|
185
|
+
elif rinex_band == 5:
|
|
186
|
+
band = Band.L5
|
|
187
|
+
|
|
188
|
+
elif constellation == ConstellationId.QZSS.value:
|
|
189
|
+
if rinex_band == 1:
|
|
190
|
+
band = Band.L1
|
|
191
|
+
elif rinex_band == 2:
|
|
192
|
+
band = Band.L2
|
|
193
|
+
elif rinex_band == 5:
|
|
194
|
+
band = Band.L5
|
|
195
|
+
elif rinex_band == 6:
|
|
196
|
+
band = Band.L6
|
|
197
|
+
|
|
198
|
+
if not band:
|
|
199
|
+
raise ValueError(f'Invalid band {rinex_band} for constellation {ConstellationId.from_string(constellation)}')
|
|
200
|
+
|
|
201
|
+
return band
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class TrackingChannel(object):
|
|
206
|
+
band: int
|
|
207
|
+
attribute: str
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
@deprecated("from_rinex3_code")
|
|
211
|
+
def from_observable_type(observable_type: str) -> 'TrackingChannel':
|
|
212
|
+
"""
|
|
213
|
+
Deprecated: This function is deprecated and will be removed in the future.
|
|
214
|
+
Use the from_rinex3_code() method instead.
|
|
215
|
+
|
|
216
|
+
Extract the tracking channel from a RINEX3 observable type
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
return TrackingChannel.from_rinex3_code(observable_type)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def from_rinex3_code(observable_code: str) -> 'TrackingChannel':
|
|
223
|
+
"""
|
|
224
|
+
Extract the tracking channel from a RINEX3 observable type
|
|
225
|
+
|
|
226
|
+
>>> TrackingChannel.from_rinex3_code('C1C')
|
|
227
|
+
1C
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
return TrackingChannel.from_string(observable_code[1:])
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def from_string(observable_code: str) -> 'TrackingChannel':
|
|
234
|
+
"""
|
|
235
|
+
Extract the tracking channel from a string that indicates the tracking
|
|
236
|
+
channel based on the last two characters of the corresponding RINEX3
|
|
237
|
+
code
|
|
238
|
+
|
|
239
|
+
>>> TrackingChannel.from_string('1C')
|
|
240
|
+
1C
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
band = int(observable_code[0])
|
|
244
|
+
attribute = observable_code[1]
|
|
245
|
+
return TrackingChannel(band, attribute)
|
|
246
|
+
|
|
247
|
+
def get_frequency(self, constellation: Union[ConstellationId, str]) -> int:
|
|
248
|
+
"""
|
|
249
|
+
Get the frequency (in Hz) from the tracking channel band
|
|
250
|
+
|
|
251
|
+
>>> ch = TrackingChannel(1, 'C')
|
|
252
|
+
>>> ch.get_frequency(ConstellationId.GPS)
|
|
253
|
+
1575420000
|
|
254
|
+
>>> ch.get_frequency('G')
|
|
255
|
+
1575420000
|
|
256
|
+
"""
|
|
257
|
+
return Band.get_freq_from_rinex_band(constellation, self.band)
|
|
258
|
+
|
|
259
|
+
def get_wavelength(self, constellation: Union[ConstellationId, str]) -> Band:
|
|
260
|
+
"""
|
|
261
|
+
Get the wavelength (in meters) from the tracking channel band
|
|
262
|
+
|
|
263
|
+
>>> ch = TrackingChannel(1, 'C')
|
|
264
|
+
>>> wl_m = ch.get_wavelength(ConstellationId.GPS)
|
|
265
|
+
>>> int(wl_m * 100)
|
|
266
|
+
19
|
|
267
|
+
>>> wl_m = ch.get_wavelength('G')
|
|
268
|
+
>>> int(wl_m * 100)
|
|
269
|
+
19
|
|
270
|
+
"""
|
|
271
|
+
return SPEED_OF_LIGHT / self.get_frequency(constellation)
|
|
272
|
+
|
|
273
|
+
def __eq__(self, other: 'TrackingChannel') -> bool:
|
|
274
|
+
return self.band == other.band and self.attribute == other.attribute
|
|
275
|
+
|
|
276
|
+
def __lt__(self, other: 'TrackingChannel') -> bool:
|
|
277
|
+
return self.attribute < other.attribute if self.band == other.band else self.band < other.band
|
|
278
|
+
|
|
279
|
+
def __repr__(self):
|
|
280
|
+
return f'{self.band:1d}{self.attribute:1s}'
|
|
281
|
+
|
|
282
|
+
def __hash__(self) -> int:
|
|
283
|
+
return hash((self.band, self.attribute))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
INVALID_TRACKING_CHANNEL = TrackingChannel(0, '0')
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class ChannelCode(Enum):
|
|
290
|
+
c_1C: str = "1C"
|
|
291
|
+
c_5Q: str = "5Q"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class Satellite:
|
|
296
|
+
constellation: ConstellationId
|
|
297
|
+
prn: int
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def from_string(satellite: str) -> 'Satellite':
|
|
301
|
+
constellation = ConstellationId.from_string(satellite[0])
|
|
302
|
+
prn = int(satellite[1:3])
|
|
303
|
+
return Satellite(constellation, prn)
|
|
304
|
+
|
|
305
|
+
def __lt__(self, other: 'Satellite') -> bool:
|
|
306
|
+
return self.prn < other.prn if self.constellation == other.constellation else self.constellation < other.constellation
|
|
307
|
+
|
|
308
|
+
def __eq__(self, other: 'Satellite') -> bool:
|
|
309
|
+
return self.constellation == other.constellation and self.prn == other.prn
|
|
310
|
+
|
|
311
|
+
def __hash__(self):
|
|
312
|
+
return hash(self.constellation) + self.prn
|
|
313
|
+
|
|
314
|
+
def __repr__(self) -> str:
|
|
315
|
+
return f"{self.constellation.value:1s}{self.prn:02d}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class Signal:
|
|
320
|
+
satellite: Satellite
|
|
321
|
+
channel: ChannelCode
|
|
322
|
+
|
|
323
|
+
def __repr__(self) -> str:
|
|
324
|
+
return f"{self.satellite}{self.channel.value}"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class GnssSystem:
|
|
328
|
+
constellation_id: ConstellationId
|
|
329
|
+
number_satellites: int
|
|
330
|
+
channels: List[ChannelCode]
|
|
331
|
+
signals: List[Signal]
|
|
332
|
+
|
|
333
|
+
def __init__(self, constellation_id: ConstellationId, number_satellites: int, channels: List[ChannelCode]):
|
|
334
|
+
self.constellation_id = constellation_id
|
|
335
|
+
self.number_satellites = number_satellites
|
|
336
|
+
self.channels = channels
|
|
337
|
+
self.signals = self.get_signals()
|
|
338
|
+
|
|
339
|
+
def get_signals(self) -> List[Signal]:
|
|
340
|
+
signals = []
|
|
341
|
+
for index in range(1, self.number_satellites + 1):
|
|
342
|
+
for channel in self.channels:
|
|
343
|
+
signals.append(Signal(Satellite(self.constellation_id, index), channel))
|
|
344
|
+
|
|
345
|
+
return signals
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass
|
|
349
|
+
class GnssSystems:
|
|
350
|
+
gnss_systems: List[GnssSystem]
|
|
351
|
+
|
|
352
|
+
def get_constellations(self) -> List[str]:
|
|
353
|
+
return [system.constellation_id.value for system in self.gnss_systems]
|
|
354
|
+
|
|
355
|
+
def get_signals(self) -> List[Signal]:
|
|
356
|
+
signals = []
|
|
357
|
+
for system in self.gnss_systems:
|
|
358
|
+
signals.extend(system.get_signals())
|
|
359
|
+
return signals
|
pygnss/hatanaka.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from pygnss._c_ext import _read_crx # compiled C extension for fast CRX parsing
|
|
7
|
+
except Exception as exc: # pragma: no cover - environment-specific
|
|
8
|
+
_import_err = exc
|
|
9
|
+
|
|
10
|
+
def _read_crx(*args, **kwargs):
|
|
11
|
+
"""Fallback stub when the compiled extension is missing.
|
|
12
|
+
|
|
13
|
+
The real implementation lives in the compiled extension module
|
|
14
|
+
``pygnss._c_ext``. If that extension isn't available (for example on
|
|
15
|
+
CI or when the package wasn't built/installed), attempting to read a
|
|
16
|
+
CRX file will raise a clear ImportError with instructions.
|
|
17
|
+
"""
|
|
18
|
+
raise ImportError(
|
|
19
|
+
"pygnss C extension 'pygnss._c_ext' is not available.\n"
|
|
20
|
+
"To enable Hatanaka (.crx) parsing, build and install the package so\n"
|
|
21
|
+
"that the compiled extension is present (e.g. running 'pip install .',\n"
|
|
22
|
+
"or building the wheel in your CI).\n"
|
|
23
|
+
f"Original import error: {type(_import_err).__name__}: {_import_err}"
|
|
24
|
+
) from _import_err
|
|
25
|
+
|
|
26
|
+
def to_dataframe(filename:str, station:str = "none", strict_lli: bool = True) -> pd.DataFrame:
|
|
27
|
+
"""
|
|
28
|
+
Convert a Compressed (crx.gz) or uncompressed (crx) Hatanaka file into a
|
|
29
|
+
DataFrame
|
|
30
|
+
|
|
31
|
+
:param filename: Hatanaka [gzip compressed] filename
|
|
32
|
+
:param station: force station name
|
|
33
|
+
:param strict_lli: Mark cycle slips only when Phase LLI is 1 (as per RINEX convention).
|
|
34
|
+
If False, any value of Phase LLI will trigger a cycle slip flag
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if filename.endswith('crx.gz') or filename.endswith('crx.Z') or filename.endswith('crz'):
|
|
38
|
+
try:
|
|
39
|
+
with gzip.open(filename, 'rb') as f_in:
|
|
40
|
+
with tempfile.NamedTemporaryFile(delete=False) as f_out:
|
|
41
|
+
f_out.write(f_in.read())
|
|
42
|
+
f_out.seek(0)
|
|
43
|
+
array = _read_crx(f_out.name)
|
|
44
|
+
except gzip.BadGzipFile:
|
|
45
|
+
raise ValueError(f"{filename} is not a valid gzip file.")
|
|
46
|
+
|
|
47
|
+
else:
|
|
48
|
+
array = _read_crx(filename)
|
|
49
|
+
|
|
50
|
+
df = pd.DataFrame(array, columns=['epoch', 'sat', 'rinex3_code', 'value', 'lli'])
|
|
51
|
+
df['channel'] = df['rinex3_code'].str[-2:]
|
|
52
|
+
df['signal'] = df['sat'] + df['channel']
|
|
53
|
+
MAPPING = {'C': 'range', 'L': 'phase', 'D': 'doppler', 'S': 'snr'}
|
|
54
|
+
df['obstype'] = df['rinex3_code'].str[0].map(lambda x: MAPPING.get(x, 'Unknown'))
|
|
55
|
+
df = df.pivot_table(index=['epoch', 'signal', 'sat', 'channel'], columns=['obstype'], values=['value', 'lli'])
|
|
56
|
+
|
|
57
|
+
# Remove all LLI columns except for the phase (for the cycle slips)
|
|
58
|
+
if strict_lli:
|
|
59
|
+
df['cslip'] = (df.loc[:, pd.IndexSlice['lli', 'phase']] % 2) == 1
|
|
60
|
+
else:
|
|
61
|
+
df['cslip'] = df.loc[:, pd.IndexSlice['lli', 'phase']] > 0
|
|
62
|
+
|
|
63
|
+
df.drop('lli', axis=1, inplace=True)
|
|
64
|
+
df.columns = [v[1] if v[0] == 'value' else v[0] for v in df.columns.values]
|
|
65
|
+
|
|
66
|
+
df.reset_index(inplace=True)
|
|
67
|
+
|
|
68
|
+
df['station'] = station
|
|
69
|
+
|
|
70
|
+
return df
|