pygnss 0.0.0__cp310-cp310-musllinux_1_2_i686.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pygnss might be problematic. Click here for more details.
- pygnss/__init__.py +1 -0
- pygnss/_c_ext/src/hatanaka.c +93 -0
- pygnss/_c_ext/src/helpers.c +17 -0
- pygnss/_c_ext/src/mtable_init.c +44 -0
- pygnss/_c_ext.cpython-310-i386-linux-gnu.so +0 -0
- pygnss/cl.py +148 -0
- pygnss/constants.py +4 -0
- pygnss/decorator.py +14 -0
- pygnss/file.py +36 -0
- pygnss/geodetic.py +1169 -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 +35 -0
- pygnss/logger.py +70 -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-0.0.0.dist-info/LICENSE +21 -0
- pygnss-0.0.0.dist-info/METADATA +67 -0
- pygnss-0.0.0.dist-info/RECORD +32 -0
- pygnss-0.0.0.dist-info/WHEEL +5 -0
- pygnss-0.0.0.dist-info/entry_points.txt +7 -0
- pygnss-0.0.0.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,35 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
from pygnss._c_ext import _read_crx
|
|
6
|
+
|
|
7
|
+
def to_dataframe(filename:str, station:str = "none") -> pd.DataFrame:
|
|
8
|
+
"""
|
|
9
|
+
Convert a Compressed (crx.gz) or uncompressed (crx) Hatanaka file into a
|
|
10
|
+
DataFrame
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
if filename.endswith('crx.gz') or filename.endswith('crx.Z') or filename.endswith('crz'):
|
|
14
|
+
try:
|
|
15
|
+
with gzip.open(filename, 'rb') as f_in:
|
|
16
|
+
with tempfile.NamedTemporaryFile(delete=False) as f_out:
|
|
17
|
+
f_out.write(f_in.read())
|
|
18
|
+
f_out.seek(0)
|
|
19
|
+
array = _read_crx(f_out.name)
|
|
20
|
+
except gzip.BadGzipFile:
|
|
21
|
+
raise ValueError(f"{filename} is not a valid gzip file.")
|
|
22
|
+
|
|
23
|
+
else:
|
|
24
|
+
array = _read_crx(filename)
|
|
25
|
+
|
|
26
|
+
df = pd.DataFrame(array, columns=['epoch', 'sat', 'rinex3_code', 'value'])
|
|
27
|
+
df['channel'] = df['rinex3_code'].str[-2:]
|
|
28
|
+
df['signal'] = df['sat'] + df['channel']
|
|
29
|
+
MAPPING = {'C': 'range', 'L': 'phase', 'D': 'doppler', 'S': 'snr'}
|
|
30
|
+
df['obstype'] = df['rinex3_code'].str[0].map(lambda x: MAPPING.get(x, 'Unknown'))
|
|
31
|
+
df = df.pivot_table(index=['epoch', 'signal', 'sat', 'channel'], columns=['obstype'], values='value')
|
|
32
|
+
df.reset_index(inplace=True)
|
|
33
|
+
df['station'] = station
|
|
34
|
+
|
|
35
|
+
return df
|
pygnss/logger.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# TODO Move logging configuration to log.ini
|
|
6
|
+
# TODO Use slack handler for notifying error to slack rokubun group
|
|
7
|
+
|
|
8
|
+
FORMAT = '%(asctime)s - %(levelname)-8s - %(message)s'
|
|
9
|
+
EPOCH_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
logger.setLevel(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
13
|
+
console_handler = logging.StreamHandler()
|
|
14
|
+
formatter = logging.Formatter(FORMAT)
|
|
15
|
+
console_handler.setFormatter(formatter)
|
|
16
|
+
logger.addHandler(console_handler)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LevelLogFilter(object):
|
|
20
|
+
def __init__(self, levels):
|
|
21
|
+
self.__levels = levels
|
|
22
|
+
|
|
23
|
+
def filter(self, record):
|
|
24
|
+
return record.levelno in self.__levels
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def debug(message):
|
|
28
|
+
logger.debug(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def info(message):
|
|
32
|
+
logger.info(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def warning(message):
|
|
36
|
+
logger.warning(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def error(message):
|
|
40
|
+
logger.error(message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def critical(message, exception=None):
|
|
44
|
+
logger.critical(message, exc_info=exception)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def exception(message, exception):
|
|
48
|
+
logger.critical(message, exc_info=exception)
|
|
49
|
+
raise exception
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def log(level, message):
|
|
53
|
+
logger.log(logging._nameToLevel[level], message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_level(level):
|
|
57
|
+
logger.setLevel(level=level)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setFileHandler(filename):
|
|
61
|
+
handler = logging.FileHandler(filename)
|
|
62
|
+
handler.setLevel(logging.DEBUG)
|
|
63
|
+
handler.setFormatter(logging.Formatter(FORMAT))
|
|
64
|
+
logger.addHandler(handler)
|
|
65
|
+
return handler
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unsetHandler(handler):
|
|
69
|
+
handler.close()
|
|
70
|
+
logger.removeHandler(handler)
|
pygnss/orbit/__init__.py
ADDED
|
File without changes
|
pygnss/orbit/kepler.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import datetime
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from ..constants import EARTH_GRAVITATION_PARAM_MU
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Kepler(object):
|
|
10
|
+
"""
|
|
11
|
+
Data class to represent a satellite orbit in an inertial reference frame
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
""" Time of Ephemeris """
|
|
15
|
+
toe: datetime.datetime
|
|
16
|
+
|
|
17
|
+
""" Semi-major axis [m] """
|
|
18
|
+
a_m: float
|
|
19
|
+
|
|
20
|
+
""" Eccentricity """
|
|
21
|
+
eccentricity: float
|
|
22
|
+
|
|
23
|
+
""" Inclination [rad] """
|
|
24
|
+
inclination_rad: float
|
|
25
|
+
|
|
26
|
+
""" Right ascension of the ascending node [rad] """
|
|
27
|
+
raan_rad: float
|
|
28
|
+
|
|
29
|
+
""" Argument of the perigee [rad] """
|
|
30
|
+
arg_perigee_rad: float
|
|
31
|
+
|
|
32
|
+
""" True anomaly at the time of ephemeris [rad] """
|
|
33
|
+
true_anomaly_rad: float
|
|
34
|
+
|
|
35
|
+
delta_n_dot_rad_per_s: float = 0.0
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
out = f"""
|
|
39
|
+
toe: {self.toe}
|
|
40
|
+
semi-major axis[deg]: {math.degrees(self.a_m)}
|
|
41
|
+
eccentricity: {self.eccentricity}
|
|
42
|
+
inclination[deg]: {math.degrees(self.inclination_rad)}
|
|
43
|
+
RAAN[deg]: {math.degrees(self.raan_rad)}
|
|
44
|
+
arg_perigee[deg]: {math.degrees(self.arg_perigee_rad)}
|
|
45
|
+
mean anomaly[deg]: {math.degrees(self.true_anomaly_rad)}
|
|
46
|
+
Delta N [deg/s]: {math.degrees(self.delta_n_dot_rad_per_s)}
|
|
47
|
+
"""
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compute_semi_major_axis(mean_motion_rad_per_s: float) -> float:
|
|
52
|
+
"""
|
|
53
|
+
Compute the semi major axis (a) in meters from the mean motion
|
|
54
|
+
|
|
55
|
+
Using the equation $$n = sqrt(mu) / (sqrt(A))^3$$
|
|
56
|
+
|
|
57
|
+
>>> mean_motion_rev_per_day = 15.5918272
|
|
58
|
+
>>> mean_motion_rad_per_s = mean_motion_rev_per_day * math.tau / 86400.0
|
|
59
|
+
>>> compute_semi_major_axis(mean_motion_rad_per_s)
|
|
60
|
+
6768158.4970976645
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
return math.pow(EARTH_GRAVITATION_PARAM_MU/math.pow(mean_motion_rad_per_s, 2.0), 1.0 / 3.0)
|