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/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)