pygnss 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.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 CHANGED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
pygnss/gnss/edit.py ADDED
@@ -0,0 +1,63 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ ARC_ID_FIELD = 'arc_id'
5
+
6
+ def compute_phase_arc_id(data: pd.DataFrame) -> pd.DataFrame:
7
+ """
8
+ Computes the phase arc ID, that can be used later to perform operations
9
+ on a per-arc basis (compute arc bias, ...)
10
+ """
11
+
12
+ data[ARC_ID_FIELD] = data.groupby('signal')['slip'].transform('cumsum')
13
+
14
+ return data
15
+
16
+
17
+ def mark_time_gap(data: pd.DataFrame, threshold_s: float = 5) -> pd.DataFrame:
18
+ """
19
+ Mark a phase cycle slip when the time series show a time gap
20
+ of a given threshold
21
+ """
22
+
23
+ # Function to mark epochs with a difference > threshold
24
+ def mark_epochs(group):
25
+ EPOCH_FIELD = 'epoch'
26
+ marked = group[EPOCH_FIELD].diff() > pd.Timedelta(seconds=threshold_s)
27
+ return marked
28
+
29
+ # Apply the function per group using groupby
30
+ marked_epochs = data.groupby('signal').apply(mark_epochs)
31
+
32
+ data['slip'] = np.any([data['slip'], marked_epochs], axis=0)
33
+
34
+ data = compute_phase_arc_id(data)
35
+
36
+ return data
37
+
38
+ def detrend(data:pd.DataFrame, observable: str, n_samples:int) -> pd.DataFrame:
39
+ """
40
+ Detrend a given observable by using a rolling window of a certain number of
41
+ samples
42
+ """
43
+
44
+ trend_column = f'{observable}_trend'
45
+ detrended_column = f'{observable}_detrended'
46
+
47
+ trend = data.groupby(['signal', ARC_ID_FIELD])[observable].transform(lambda x: x.rolling(n_samples).mean())
48
+ data[trend_column] = trend
49
+ data[detrended_column] = data[observable] - data[trend_column]
50
+
51
+ return data
52
+
53
+ def remove_mean(data: pd.DataFrame, observable: str) -> pd.DataFrame:
54
+
55
+ bias_field = f'{observable}_bias'
56
+ aligned_field = f'{observable}_aligned'
57
+
58
+ # For each arch id, compute the median
59
+ data[bias_field] = data.groupby(['signal',ARC_ID_FIELD])[observable].transform('mean')
60
+
61
+ data[aligned_field] = data[observable] - data[bias_field]
62
+
63
+ 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) & (df['channel'] == str(channel_a))]
17
+ df_b = df[(df['constellation'] == constellation) & (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/types.py ADDED
@@ -0,0 +1,334 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass
3
+ from typing import List
4
+
5
+ from roktools.constants import SPEED_OF_LIGHT
6
+ from roktools.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
+ STARLINK: str = 'V'
19
+ SPIRE: str = 'Z'
20
+ LEO: str = 'L'
21
+ ONEWEB: str = 'O'
22
+
23
+ @staticmethod
24
+ def from_string(constellation_char: str) -> 'ConstellationId':
25
+ """
26
+ Returns the corresponding ConstellationId from input character
27
+
28
+ >>> ConstellationId.from_string('E')
29
+ <ConstellationId.GALILEO: 'E'>
30
+ """
31
+ try:
32
+ return ConstellationId(constellation_char)
33
+ except ValueError:
34
+ raise ValueError(f"Invalid constellation character: {constellation_char}")
35
+
36
+ def to_char(self):
37
+ """
38
+ Constellation representation as character
39
+
40
+ >>> c = ConstellationId.GPS
41
+ >>> c.to_char()
42
+ 'G'
43
+ """
44
+
45
+ return self.value
46
+
47
+ def __lt__(self, other):
48
+ """
49
+ Less operator for the ConstellationId class
50
+
51
+ >>> c1 = ConstellationId.GPS
52
+ >>> c2 = ConstellationId.IRNSS
53
+ >>> c1 < c2
54
+ True
55
+ >>> c2 < c1
56
+ False
57
+ >>> c2 < 0
58
+ Traceback (most recent call last):
59
+ ...
60
+ TypeError: '<' not supported between instances of 'ConstellationId' and 'int'
61
+ """
62
+
63
+ if isinstance(other, ConstellationId):
64
+ return self.value < other.value
65
+ return NotImplemented
66
+
67
+
68
+ class Band:
69
+ """
70
+ Namespace to define the supported bands (center frequency in Hz)
71
+ """
72
+
73
+ L1 = 1575420000
74
+ L2 = 1227600000
75
+ L5 = 1176450000
76
+ L6 = 1278750000
77
+ G1 = 1602000000
78
+ G1a = 1600995000
79
+ G2 = 1246000000
80
+ G2a = 1248060000
81
+ G3 = 1202025000
82
+ E1 = 1575420000
83
+ E5a = 1176450000
84
+ E5b = 1207140000
85
+ E5 = 1191795000
86
+ E6 = 1278750000
87
+ B1_2 = 1561098000
88
+ B1 = 1575420000
89
+ B2a = 1176450000
90
+ B2b = 1207140000
91
+ B2 = 1191795000
92
+ B3 = 1268520000
93
+ S = 2492028000
94
+
95
+ def __init__(self):
96
+ """
97
+ Init the class (does nothing)
98
+ """
99
+ pass
100
+
101
+ @staticmethod
102
+ def get_freq_from_rinex_band(constellation: ConstellationId, rinex_band: int) -> int:
103
+ """
104
+ Get the band from the RINEX band number (and constellation)
105
+
106
+ >>> Band.get_freq_from_rinex_band(ConstellationId.GPS, 1)
107
+ 1575420000
108
+ >>> Band.get_freq_from_rinex_band(ConstellationId.GALILEO, 5)
109
+ 1176450000
110
+ >>> Band.get_freq_from_rinex_band(ConstellationId.GPS, 8)
111
+ Traceback (most recent call last):
112
+ ...
113
+ ValueError: Invalid band 8 for constellation ConstellationId.GPS
114
+ """
115
+
116
+ band = None
117
+
118
+ if constellation == ConstellationId.GPS:
119
+ if rinex_band == 1:
120
+ band = Band.L1
121
+ elif rinex_band == 2:
122
+ band = Band.L2
123
+ elif rinex_band == 5:
124
+ band = Band.L5
125
+ elif rinex_band == 6:
126
+ band = Band.L6
127
+
128
+ elif constellation == ConstellationId.GLONASS:
129
+ if rinex_band == 1:
130
+ band = Band.G1
131
+ elif rinex_band == 4:
132
+ band = Band.G1a
133
+ elif rinex_band == 2:
134
+ band = Band.G2
135
+ elif rinex_band == 6:
136
+ band = Band.G2a
137
+ elif rinex_band == 3:
138
+ band = Band.G3
139
+
140
+ elif constellation == ConstellationId.GALILEO:
141
+ if rinex_band == 1:
142
+ band = Band.E1
143
+ elif rinex_band == 5:
144
+ band = Band.E5a
145
+ elif rinex_band == 7:
146
+ band = Band.E5b
147
+ elif rinex_band == 8:
148
+ band = Band.E5
149
+ elif rinex_band == 6:
150
+ band = Band.E6
151
+
152
+ elif constellation == ConstellationId.BEIDOU:
153
+ if rinex_band == 2:
154
+ band = Band.B1_2
155
+ elif rinex_band == 1:
156
+ band = Band.B1 = 1575420000
157
+ elif rinex_band == 5:
158
+ band = Band.B2a = 1176450000
159
+ elif rinex_band == 7:
160
+ band = Band.B2b = 1207140000
161
+ elif rinex_band == 8:
162
+ band = Band.B2 = 1191795000
163
+ elif rinex_band == 6:
164
+ band = Band.B3 = 1268520000
165
+
166
+ elif constellation == ConstellationId.IRNSS:
167
+ if rinex_band == 5:
168
+ band = Band.L5
169
+ elif rinex_band == 9:
170
+ band = Band.S
171
+
172
+ elif constellation == ConstellationId.SBAS:
173
+ if rinex_band == 1:
174
+ band = Band.L1
175
+ elif rinex_band == 5:
176
+ band = Band.L5
177
+
178
+ if not band:
179
+ raise ValueError(f'Invalid band {rinex_band} for constellation {constellation}')
180
+
181
+ return band
182
+
183
+
184
+ @dataclass
185
+ class TrackingChannel(object):
186
+ band: int
187
+ attribute: str
188
+
189
+ @staticmethod
190
+ @deprecated("from_rinex3_code")
191
+ def from_observable_type(observable_type: str) -> 'TrackingChannel':
192
+ """
193
+ Deprecated: This function is deprecated and will be removed in the future.
194
+ Use the from_rinex3_code() method instead.
195
+
196
+ Extract the tracking channel from a RINEX3 observable type
197
+ """
198
+
199
+ return TrackingChannel.from_rinex3_code(observable_type)
200
+
201
+ @staticmethod
202
+ def from_rinex3_code(observable_code: str) -> 'TrackingChannel':
203
+ """
204
+ Extract the tracking channel from a RINEX3 observable type
205
+
206
+ >>> TrackingChannel.from_rinex3_code('C1C')
207
+ 1C
208
+ """
209
+
210
+ return TrackingChannel.from_string(observable_code[1:])
211
+
212
+ @staticmethod
213
+ def from_string(observable_code: str) -> 'TrackingChannel':
214
+ """
215
+ Extract the tracking channel from a string that indicates the tracking
216
+ channel based on the last two characters of the corresponding RINEX3
217
+ code
218
+
219
+ >>> TrackingChannel.from_string('1C')
220
+ 1C
221
+ """
222
+
223
+ band = int(observable_code[0])
224
+ attribute = observable_code[1]
225
+ return TrackingChannel(band, attribute)
226
+
227
+ def get_frequency(self, constellation: ConstellationId) -> int:
228
+ """
229
+ Get the frequency (in Hz) from the tracking channel band
230
+
231
+ >>> ch = TrackingChannel(1, 'C')
232
+ >>> ch.get_frequency(ConstellationId.GPS)
233
+ 1575420000
234
+ """
235
+ return Band.get_freq_from_rinex_band(constellation, self.band)
236
+
237
+ def get_wavelength(self, constellation: ConstellationId) -> Band:
238
+ """
239
+ Get the wavelength (in meters) from the tracking channel band
240
+
241
+ >>> ch = TrackingChannel(1, 'C')
242
+ >>> wl_m = ch.get_wavelength(ConstellationId.GPS)
243
+ >>> int(wl_m * 100)
244
+ 19
245
+ """
246
+ return SPEED_OF_LIGHT / self.get_frequency(constellation)
247
+
248
+ def __eq__(self, other: 'TrackingChannel') -> bool:
249
+ return self.band == other.band and self.attribute == other.attribute
250
+
251
+ def __lt__(self, other: 'TrackingChannel') -> bool:
252
+ return self.attribute < other.attribute if self.band == other.band else self.band < other.band
253
+
254
+ def __repr__(self):
255
+ return f'{self.band:1d}{self.attribute:1s}'
256
+
257
+ def __hash__(self) -> int:
258
+ return hash((self.band, self.attribute))
259
+
260
+
261
+ INVALID_TRACKING_CHANNEL = TrackingChannel(0, '0')
262
+
263
+
264
+ class ChannelCode(Enum):
265
+ c_1C: str = "1C"
266
+ c_5Q: str = "5Q"
267
+
268
+
269
+ @dataclass
270
+ class Satellite:
271
+ constellation: ConstellationId
272
+ prn: int
273
+
274
+ @staticmethod
275
+ def from_string(satellite: str) -> 'Satellite':
276
+ constellation = ConstellationId.from_string(satellite[0])
277
+ prn = int(satellite[1:3])
278
+ return Satellite(constellation, prn)
279
+
280
+ def __lt__(self, other: 'Satellite') -> bool:
281
+ return self.prn < other.prn if self.constellation == other.constellation else self.constellation < other.constellation
282
+
283
+ def __eq__(self, other: 'Satellite') -> bool:
284
+ return self.constellation == other.constellation and self.prn == other.prn
285
+
286
+ def __hash__(self):
287
+ return hash(self.constellation) + self.prn
288
+
289
+ def __repr__(self) -> str:
290
+ return f"{self.constellation.value:1s}{self.prn:02d}"
291
+
292
+
293
+ @dataclass
294
+ class Signal:
295
+ satellite: Satellite
296
+ channel: ChannelCode
297
+
298
+ def __repr__(self) -> str:
299
+ return f"{self.satellite}{self.channel.value}"
300
+
301
+
302
+ class GnssSystem:
303
+ constellation_id: ConstellationId
304
+ number_satellites: int
305
+ channels: List[ChannelCode]
306
+ signals: List[Signal]
307
+
308
+ def __init__(self, constellation_id: ConstellationId, number_satellites: int, channels: List[ChannelCode]):
309
+ self.constellation_id = constellation_id
310
+ self.number_satellites = number_satellites
311
+ self.channels = channels
312
+ self.signals = self.get_signals()
313
+
314
+ def get_signals(self) -> List[Signal]:
315
+ signals = []
316
+ for index in range(1, self.number_satellites + 1):
317
+ for channel in self.channels:
318
+ signals.append(Signal(Satellite(self.constellation_id, index), channel))
319
+
320
+ return signals
321
+
322
+
323
+ @dataclass
324
+ class GnssSystems:
325
+ gnss_systems: List[GnssSystem]
326
+
327
+ def get_constellations(self) -> List[str]:
328
+ return [system.constellation_id.value for system in self.gnss_systems]
329
+
330
+ def get_signals(self) -> List[Signal]:
331
+ signals = []
332
+ for system in self.gnss_systems:
333
+ signals.extend(system.get_signals())
334
+ return signals
pygnss/helpers.py ADDED
@@ -0,0 +1,20 @@
1
+ from typing import Iterable
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+
6
+ def compute_elapsed_seconds(epochs:pd.Series) -> pd.Series:
7
+ return (epochs - epochs.iloc[0]).dt.total_seconds()
8
+
9
+ def compute_decimal_hours(epochs:pd.Series) -> pd.Series:
10
+ return epochs.apply(lambda x: x.hour + x.minute / 60 + x.second / 3600)
11
+
12
+ def compute_rms(values:Iterable) -> float:
13
+ """
14
+ Compute the Root Mean Square of an array of values
15
+
16
+ >>> array = [1, 2, 3, 4, 5]
17
+ >>> compute_rms(array)
18
+ 3.3166247903554
19
+ """
20
+ return np.sqrt(np.mean(np.square(values)))
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygnss
3
- Version: 0.1.0
4
- Summary:
3
+ Version: 0.2.0
4
+ Summary: Package with utilities for GNSS data processing
5
5
  Author: Miquel Garcia
6
6
  Author-email: miquel.garcia@rokubun.cat
7
7
  Requires-Python: >=3.10,<4.0
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Dist: numpy (>=1.26.4,<2.0.0)
13
13
  Requires-Dist: pandas (>=2.2.1,<3.0.0)
14
+ Requires-Dist: roktools (>=5.30.1,<6.0.0)
14
15
  Description-Content-Type: text/markdown
15
16
 
16
17
  # Python GNSS Data processing library
@@ -0,0 +1,10 @@
1
+ pygnss/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ pygnss/gnss/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pygnss/gnss/edit.py,sha256=YcCKJZyyDjR7iX8uJ5z7dmajNY6mNqrmDuX0N741tPs,1877
4
+ pygnss/gnss/observables.py,sha256=TsM6_u51PTZamat_dUysg6fchfVX9eJ3A3C27bbhEC0,1643
5
+ pygnss/gnss/types.py,sha256=j782g6gl7sD4FLzX_UQydrXFBonHa0qVoZ9EIOJ_-7o,9505
6
+ pygnss/helpers.py,sha256=cXbogBg7U3QY06eR80FQ4DTEmeu-NH-BijIIbi0iX1Q,561
7
+ pygnss-0.2.0.dist-info/LICENSE,sha256=1zfF0G6hMAEdsZfmxhznzXDlfGfX8KXeEGIVvyhzbZQ,1064
8
+ pygnss-0.2.0.dist-info/METADATA,sha256=S3YNmPa5XJsi_ghVn6Fb-fypT3zwZ7LftBaiLMuoc_U,721
9
+ pygnss-0.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
+ pygnss-0.2.0.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- pygnss/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pygnss-0.1.0.dist-info/LICENSE,sha256=1zfF0G6hMAEdsZfmxhznzXDlfGfX8KXeEGIVvyhzbZQ,1064
3
- pygnss-0.1.0.dist-info/METADATA,sha256=I-2UMyqJadDsC5ZyJ66x7hjpRR3LKy0yBajvwFKZ0zk,632
4
- pygnss-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
5
- pygnss-0.1.0.dist-info/RECORD,,
File without changes