pygnss 2.1.2__cp314-cp314t-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygnss/__init__.py +1 -0
- pygnss/_c_ext/src/constants.c +36 -0
- pygnss/_c_ext/src/hatanaka.c +94 -0
- pygnss/_c_ext/src/helpers.c +17 -0
- pygnss/_c_ext/src/klobuchar.c +313 -0
- pygnss/_c_ext/src/mtable_init.c +50 -0
- pygnss/_c_ext.cpython-314t-darwin.so +0 -0
- pygnss/cl.py +148 -0
- pygnss/constants.py +4 -0
- pygnss/decorator.py +47 -0
- pygnss/file.py +36 -0
- pygnss/filter/__init__.py +77 -0
- pygnss/filter/ekf.py +80 -0
- pygnss/filter/models.py +74 -0
- pygnss/filter/particle.py +484 -0
- pygnss/filter/ukf.py +322 -0
- pygnss/geodetic.py +1177 -0
- pygnss/gnss/__init__.py +0 -0
- pygnss/gnss/edit.py +66 -0
- pygnss/gnss/observables.py +43 -0
- pygnss/gnss/residuals.py +43 -0
- pygnss/gnss/types.py +359 -0
- pygnss/hatanaka.py +70 -0
- pygnss/ionex.py +410 -0
- pygnss/iono/__init__.py +47 -0
- pygnss/iono/chapman.py +35 -0
- pygnss/iono/gim.py +131 -0
- pygnss/logger.py +70 -0
- pygnss/nequick.py +57 -0
- pygnss/orbit/__init__.py +0 -0
- pygnss/orbit/kepler.py +63 -0
- pygnss/orbit/tle.py +186 -0
- pygnss/parsers/rtklib/stats.py +166 -0
- pygnss/rinex.py +2161 -0
- pygnss/sinex.py +121 -0
- pygnss/stats.py +75 -0
- pygnss/tensorial.py +50 -0
- pygnss/time.py +350 -0
- pygnss-2.1.2.dist-info/METADATA +129 -0
- pygnss-2.1.2.dist-info/RECORD +44 -0
- pygnss-2.1.2.dist-info/WHEEL +6 -0
- pygnss-2.1.2.dist-info/entry_points.txt +8 -0
- pygnss-2.1.2.dist-info/licenses/LICENSE +21 -0
- pygnss-2.1.2.dist-info/top_level.txt +1 -0
pygnss/nequick.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import math
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import nequick
|
|
8
|
+
|
|
9
|
+
from pygnss import ionex
|
|
10
|
+
import pygnss.iono.gim
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GimIonexHandler(nequick.GimHandler):
|
|
14
|
+
"""
|
|
15
|
+
A handler that accumulates GIMs and then generates an IONEX file
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, coeffs: nequick.Coefficients):
|
|
19
|
+
self._coeffs = coeffs
|
|
20
|
+
self._gims: List[nequick.gim.Gim] = []
|
|
21
|
+
|
|
22
|
+
def process(self, gim: nequick.Gim):
|
|
23
|
+
"""
|
|
24
|
+
Store the incoming gim for later process
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Check that the latitude and longitude values are
|
|
28
|
+
# the same as the last appended gim
|
|
29
|
+
if len(self._gims) > 0:
|
|
30
|
+
last_gim = self._gims[-1]
|
|
31
|
+
if np.array_equal(last_gim.latitudes, gim.latitudes) == False:
|
|
32
|
+
raise ValueError("Latitude values do not match")
|
|
33
|
+
if np.array_equal(last_gim.longitudes, gim.longitudes) == False:
|
|
34
|
+
raise ValueError("Longitude values do not match")
|
|
35
|
+
|
|
36
|
+
self._gims.append(gim)
|
|
37
|
+
|
|
38
|
+
def to_ionex(self, filename: str, pgm: str = "pygnss", runby: str = "pygnss") -> None:
|
|
39
|
+
|
|
40
|
+
comment_lines = [
|
|
41
|
+
"Maps computed using the NeQuick model with the following",
|
|
42
|
+
"coefficients:",
|
|
43
|
+
f"a0={self._coeffs.a0:<17.6f}a1={self._coeffs.a1:<17.8f}a2={self._coeffs.a2:<17.11f}"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ionex.write(filename, self._gims, pygnss.iono.gim.GimType.TEC, pgm, runby,
|
|
47
|
+
comment_lines=comment_lines)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_ionex(filename: str, coeffs: nequick.Coefficients, dates: List[datetime.datetime]):
|
|
51
|
+
|
|
52
|
+
gim_handler = GimIonexHandler(coeffs)
|
|
53
|
+
|
|
54
|
+
for date in dates:
|
|
55
|
+
nequick.to_gim(coeffs, date, gim_handler=gim_handler)
|
|
56
|
+
|
|
57
|
+
gim_handler.to_ionex(filename)
|
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)
|
pygnss/orbit/tle.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import math
|
|
3
|
+
from typing import List, Union, IO
|
|
4
|
+
|
|
5
|
+
from ..gnss.types import ConstellationId, Satellite
|
|
6
|
+
|
|
7
|
+
from .kepler import Kepler, compute_semi_major_axis
|
|
8
|
+
|
|
9
|
+
REV_PER_DAY_TO_RAD_PER_S = math.tau / 86400.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_decimal_point(number: str) -> float:
|
|
13
|
+
"""
|
|
14
|
+
Parse float numers with decimal point assumed and exponent
|
|
15
|
+
|
|
16
|
+
>>> parse_decimal_point("0006703")
|
|
17
|
+
0.0006703
|
|
18
|
+
>>> f'{parse_decimal_point("-11606-4"):.4e}'
|
|
19
|
+
'-1.1606e-05'
|
|
20
|
+
>>> f'{parse_decimal_point("-11606+4"):.4e}'
|
|
21
|
+
'-1.1606E+03'
|
|
22
|
+
>>> f'{parse_decimal_point(" 11606-4"):.4e}'
|
|
23
|
+
'1.1606e-05'
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
has_exponent = number[-2] == '-' or number[-2] == '+'
|
|
27
|
+
|
|
28
|
+
if has_exponent:
|
|
29
|
+
power = math.pow(10, int(number[-2:]))
|
|
30
|
+
n = len(number[1:-2])
|
|
31
|
+
return float(number[0:-2]) * math.pow(10, -n) * power
|
|
32
|
+
else:
|
|
33
|
+
power = math.pow(10, -len(number))
|
|
34
|
+
return float(number) * power
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def calculate_checksum(line: str) -> int:
|
|
38
|
+
checksum = 0
|
|
39
|
+
|
|
40
|
+
for character in line:
|
|
41
|
+
if character.isdigit():
|
|
42
|
+
checksum += int(character)
|
|
43
|
+
elif character == '-':
|
|
44
|
+
checksum += 1
|
|
45
|
+
|
|
46
|
+
return checksum % 10
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TLE(object):
|
|
50
|
+
|
|
51
|
+
def __init__(self, line1: str, line2: str, label=None) -> 'TLE':
|
|
52
|
+
|
|
53
|
+
# Check integrity of TLE input
|
|
54
|
+
id_1 = line1[2:7]
|
|
55
|
+
id_2 = line2[2:7]
|
|
56
|
+
if id_1 != id_2:
|
|
57
|
+
raise RuntimeError(f'TLE lines correspond to different satellite catalog numbers {id_1} == {id_2}')
|
|
58
|
+
|
|
59
|
+
chk = calculate_checksum(line1[:-1])
|
|
60
|
+
chk_expected = int(line1[-1])
|
|
61
|
+
if chk != chk_expected:
|
|
62
|
+
raise RuntimeError(f'Invalid checksum for TLE line 1, got {chk}, expected {chk_expected}')
|
|
63
|
+
chk = calculate_checksum(line2[:-1])
|
|
64
|
+
chk_expected = int(line2[-1])
|
|
65
|
+
if chk != chk_expected:
|
|
66
|
+
raise RuntimeError(f'Invalid checksum for TLE line 2, got {chk}, expected {chk_expected}')
|
|
67
|
+
|
|
68
|
+
self.label = label
|
|
69
|
+
self.id = int(id_1)
|
|
70
|
+
|
|
71
|
+
line = line1
|
|
72
|
+
|
|
73
|
+
# Epoch
|
|
74
|
+
year = int(line[18:20]) + 2000
|
|
75
|
+
day_of_year = float(line[20:32])
|
|
76
|
+
doy = int(day_of_year)
|
|
77
|
+
fraction_of_day = day_of_year - doy
|
|
78
|
+
f_hour = 24 * fraction_of_day
|
|
79
|
+
hour = int(f_hour)
|
|
80
|
+
f_min = 60 * (f_hour - hour)
|
|
81
|
+
min = int(f_min)
|
|
82
|
+
f_sec = 60 * (f_min - min)
|
|
83
|
+
sec = int(f_sec)
|
|
84
|
+
fraction_of_second = f_sec - sec
|
|
85
|
+
datetime_str = f'{year} {doy} {hour} {min} {sec}'
|
|
86
|
+
epoch = datetime.datetime.strptime(datetime_str, '%Y %j %H %M %S')
|
|
87
|
+
offset = datetime.timedelta(seconds=fraction_of_second)
|
|
88
|
+
self.toe = epoch + offset
|
|
89
|
+
|
|
90
|
+
# Mean motion
|
|
91
|
+
self.n_dot_rad_per_s = float(line[33:43]) * REV_PER_DAY_TO_RAD_PER_S
|
|
92
|
+
self.n_dot_dot_rad_per_s2 = parse_decimal_point(line[44:52]) * REV_PER_DAY_TO_RAD_PER_S / 86400.0
|
|
93
|
+
|
|
94
|
+
# Atmospheric drag coefficient
|
|
95
|
+
self.bstar = parse_decimal_point(line[53:61])
|
|
96
|
+
|
|
97
|
+
line = line2
|
|
98
|
+
|
|
99
|
+
self.inclination_rad = math.radians(float(line[8:16]))
|
|
100
|
+
self.RAAN_rad = math.radians(float(line[17:25]))
|
|
101
|
+
self.eccentricity = parse_decimal_point(line[26:33])
|
|
102
|
+
self.arg_perigee_rad = math.radians(float(line[34:42]))
|
|
103
|
+
self.mean_anomaly_rad = math.radians(float(line[43:51]))
|
|
104
|
+
self.mean_motion_rad_per_s = float(line[52:63]) * REV_PER_DAY_TO_RAD_PER_S
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
out = f"""
|
|
108
|
+
label: {self.label}
|
|
109
|
+
id: {self.id}
|
|
110
|
+
toe: {self.toe}
|
|
111
|
+
n_dot[rad/s]: {self.n_dot_rad_per_s}
|
|
112
|
+
n_dot_dot[rad/s^2]: {self.n_dot_dot_rad_per_s2}
|
|
113
|
+
b*: {self.bstar}
|
|
114
|
+
inclination[deg]: {math.degrees(self.inclination_rad)}
|
|
115
|
+
RAAN[deg]: {math.degrees(self.RAAN_rad)}
|
|
116
|
+
eccentricity: {self.eccentricity}
|
|
117
|
+
arg_perigee[deg]: {math.degrees(self.arg_perigee_rad)}
|
|
118
|
+
mean anomaly[deg]: {math.degrees(self.mean_anomaly_rad)}
|
|
119
|
+
mean motion[rad/s]: {self.mean_motion_rad_per_s}
|
|
120
|
+
"""
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
def get_satellite(self) -> Satellite:
|
|
124
|
+
|
|
125
|
+
constellation = get_constellation_from_label(self.label)
|
|
126
|
+
prn = self.id
|
|
127
|
+
|
|
128
|
+
return Satellite(constellation, prn)
|
|
129
|
+
|
|
130
|
+
def to_kepler(self) -> Kepler:
|
|
131
|
+
"""
|
|
132
|
+
Rough mapping of the Two Line Element set into Keplerian parameters
|
|
133
|
+
|
|
134
|
+
Use this method at your own risk, TLE elements cannot be considered
|
|
135
|
+
classical orbital elements
|
|
136
|
+
|
|
137
|
+
Based on https://blog.hardinglabs.com/tle-to-kep.html
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
a_m = compute_semi_major_axis(self.mean_motion_rad_per_s)
|
|
141
|
+
return Kepler(self.toe,
|
|
142
|
+
a_m, self.eccentricity, self.inclination_rad, self.RAAN_rad,
|
|
143
|
+
self.arg_perigee_rad, self.mean_anomaly_rad,
|
|
144
|
+
delta_n_dot_rad_per_s=self.n_dot_rad_per_s)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def read_celestrak(tle_source: Union[str, IO]) -> List[TLE]:
|
|
148
|
+
"""
|
|
149
|
+
Read a NORAD General Perturbations (GP) file in TLE format, that can be found
|
|
150
|
+
at https://celestrak.org/NORAD/elements/
|
|
151
|
+
"""
|
|
152
|
+
tles = []
|
|
153
|
+
|
|
154
|
+
# Check if tle_source is a string (filename) or a file handler
|
|
155
|
+
if isinstance(tle_source, str):
|
|
156
|
+
with open(tle_source, "r") as fh:
|
|
157
|
+
_read_celestrak_from_stream(fh, tles)
|
|
158
|
+
else:
|
|
159
|
+
_read_celestrak_from_stream(tle_source, tles)
|
|
160
|
+
|
|
161
|
+
return tles
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _read_celestrak_from_stream(file_handle: IO[str], tles: List[TLE]) -> None:
|
|
165
|
+
|
|
166
|
+
while True:
|
|
167
|
+
lines = [next(file_handle, None) for _ in range(3)]
|
|
168
|
+
if any(line is None for line in lines):
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
lines = [line.strip() for line in lines]
|
|
172
|
+
tles.append(TLE(lines[1], lines[2], label=lines[0]))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_constellation_from_label(label: str) -> ConstellationId:
|
|
176
|
+
|
|
177
|
+
out = ConstellationId.UNKNOWN
|
|
178
|
+
|
|
179
|
+
if 'ONEWEB' in label:
|
|
180
|
+
out = ConstellationId.ONEWEB
|
|
181
|
+
elif 'LEMUR' in label:
|
|
182
|
+
out = ConstellationId.SPIRE
|
|
183
|
+
elif 'STARLINK' in label:
|
|
184
|
+
out = ConstellationId.STARLINK
|
|
185
|
+
|
|
186
|
+
return out
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from pygnss import logger
|
|
6
|
+
from pygnss.file import grep_lines
|
|
7
|
+
from pygnss.time import from_week_tow
|
|
8
|
+
from pygnss.gnss.residuals import Residuals
|
|
9
|
+
from pygnss.gnss.types import TrackingChannel, INVALID_TRACKING_CHANNEL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse(filename: str) -> Residuals:
|
|
13
|
+
"""
|
|
14
|
+
Loads $SAT lines of a.stat file from a rtklib solution
|
|
15
|
+
The format of those files are the following:
|
|
16
|
+
Residuals of pseudorange and carrier-phase observables. The format
|
|
17
|
+
of a record is as follows.
|
|
18
|
+
$SAT,week,tow,sat,frq,az,el,resp,resc,vsat,snr,fix,slip,lock,outc,slipc,rejc,icbias,bias,bias_var,lambda
|
|
19
|
+
- week/tow : gps week no/time of week (s)
|
|
20
|
+
- sat/frq : satellite id/frequency (1:L1,2:L2,3:L5,...)
|
|
21
|
+
- az/el : azimuth/elevation angle (deg)
|
|
22
|
+
- resp : pseudorange residual (m)
|
|
23
|
+
- resc : carrier-phase residual (m)
|
|
24
|
+
- vsat : valid data flag (0:invalid,1:valid)
|
|
25
|
+
- snr : signal strength (dbHz)
|
|
26
|
+
- fix : ambiguity flag (0:no data,1:not part of AR set,2:part of AR set,3:part of hold set)
|
|
27
|
+
- slip : cycle-slip flag (bit1:slip,bit2:parity unknown)
|
|
28
|
+
- lock : carrier-lock count
|
|
29
|
+
- outc : data outage count
|
|
30
|
+
- slipc : cycle-slip count
|
|
31
|
+
- rejc : data reject (outlier) count
|
|
32
|
+
- icbias : interchannel bias (GLONASS)
|
|
33
|
+
- bias : phase bias
|
|
34
|
+
- bias_var : variance of phase bias
|
|
35
|
+
- lambda : wavelength
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
$SAT,2215,152783.000,G03,1,82.8,35.9,16.3975,0.0018,1,36,0,0,0,0,0,0,-117643836.98,182.318392,0.00000
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
stat_file (str): .stat file path
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
np.array: A numpy array with the content of the file
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
DTYPE = [
|
|
49
|
+
('week', 'i8'),
|
|
50
|
+
('tow', 'f8'),
|
|
51
|
+
('sat', 'S3'),
|
|
52
|
+
('freq', 'i8'),
|
|
53
|
+
('az', 'f8'),
|
|
54
|
+
('el', 'f8'),
|
|
55
|
+
('res_code_m', 'f8'),
|
|
56
|
+
('res_phase_m', 'f8'),
|
|
57
|
+
('vsat', 'i8'),
|
|
58
|
+
('snr_dbHz', 'f8'),
|
|
59
|
+
('fix', 'f8'),
|
|
60
|
+
('slip', 'f8'),
|
|
61
|
+
('lock', 'f8'),
|
|
62
|
+
('outc', 'f8'),
|
|
63
|
+
('slipc', 'f8'),
|
|
64
|
+
('rejc', 'f8'),
|
|
65
|
+
# ('icbias', 'f8'),
|
|
66
|
+
# ('bias', 'f8'),
|
|
67
|
+
# ('bias_var', 'f8'),
|
|
68
|
+
# ('lambda', 'f8'),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
USECOLS = list(range(1, len(DTYPE) + 1))
|
|
72
|
+
|
|
73
|
+
generator = grep_lines(filename, "$SAT")
|
|
74
|
+
|
|
75
|
+
data = np.loadtxt(generator, delimiter=',', usecols=USECOLS, dtype=DTYPE)
|
|
76
|
+
|
|
77
|
+
data = __add_field_in_numpy_array(data, [('epoch', datetime.datetime)])
|
|
78
|
+
data['epoch'] = [from_week_tow(int(row['week']), float(row['tow'])) for row in data]
|
|
79
|
+
|
|
80
|
+
data = __add_field_in_numpy_array(data, [('processing_direction', 'S8')])
|
|
81
|
+
data_length = len(data)
|
|
82
|
+
data['processing_direction'] = ['forward'] * data_length
|
|
83
|
+
if (np.unique(data['epoch']).size > 1) and (data['epoch'][0] == data['epoch'][-1]):
|
|
84
|
+
data['processing_direction'][int(data_length / 2):] = 'backward'
|
|
85
|
+
df = pd.DataFrame(data)
|
|
86
|
+
df['sat'] = df['sat'].str.decode('utf-8')
|
|
87
|
+
df['processing_direction'] = df['processing_direction'].str.decode('utf-8')
|
|
88
|
+
df['constellation'] = df['sat'].str[0]
|
|
89
|
+
df['channel'] = [__compute_channel(constellation, frequency)
|
|
90
|
+
for constellation, frequency in zip(df['constellation'], df['freq'])]
|
|
91
|
+
df['signal'] = [str(sat) + str(channel) for sat, channel in zip(df['sat'], df['channel'])]
|
|
92
|
+
|
|
93
|
+
return Residuals(df)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def __compute_channel(constellation: str, frequency: int) -> TrackingChannel:
|
|
97
|
+
|
|
98
|
+
CHANNEL_1C = TrackingChannel(1, 'C')
|
|
99
|
+
CHANNEL_2C = TrackingChannel(2, 'C')
|
|
100
|
+
CHANNEL_5Q = TrackingChannel(5, 'Q')
|
|
101
|
+
|
|
102
|
+
conversion_rule = {
|
|
103
|
+
'G': {
|
|
104
|
+
1: CHANNEL_1C,
|
|
105
|
+
2: CHANNEL_2C,
|
|
106
|
+
3: CHANNEL_5Q,
|
|
107
|
+
},
|
|
108
|
+
'E': {
|
|
109
|
+
1: CHANNEL_1C,
|
|
110
|
+
2: TrackingChannel(7, 'Q'),
|
|
111
|
+
3: CHANNEL_5Q,
|
|
112
|
+
},
|
|
113
|
+
'C': {
|
|
114
|
+
1: TrackingChannel(2, 'I'),
|
|
115
|
+
2: TrackingChannel(6, 'I'),
|
|
116
|
+
3: TrackingChannel(7, 'I'),
|
|
117
|
+
},
|
|
118
|
+
'R': {
|
|
119
|
+
1: CHANNEL_1C,
|
|
120
|
+
2: CHANNEL_2C,
|
|
121
|
+
3: INVALID_TRACKING_CHANNEL,
|
|
122
|
+
},
|
|
123
|
+
'J': {
|
|
124
|
+
1: CHANNEL_1C,
|
|
125
|
+
2: CHANNEL_2C,
|
|
126
|
+
3: CHANNEL_5Q,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
return conversion_rule[constellation][frequency]
|
|
132
|
+
except Exception:
|
|
133
|
+
logger.debug(f"Unknown channel for {constellation} with frequency {frequency}")
|
|
134
|
+
return INVALID_TRACKING_CHANNEL
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def __add_field_in_numpy_array(a: np.array, descr) -> np.array:
|
|
138
|
+
"""Return a new array that is like "a", but has additional fields.
|
|
139
|
+
|
|
140
|
+
Arguments:
|
|
141
|
+
a -- a structured numpy array
|
|
142
|
+
descr -- a numpy type description of the new fields
|
|
143
|
+
|
|
144
|
+
The contents of "a" are copied over to the appropriate fields in
|
|
145
|
+
the new array, whereas the new fields are uninitialized. The
|
|
146
|
+
arguments are not modified.
|
|
147
|
+
|
|
148
|
+
>>> sa = numpy.array([(1, 'Foo'), (2, 'Bar')], dtype=[('id', '<i8'), ('name', '|S3')])
|
|
149
|
+
>>> sa.dtype.descr
|
|
150
|
+
[('id', '<i8'), ('name', '|S3')]
|
|
151
|
+
|
|
152
|
+
>>> sb = __add_field_in_numpy_array(sa, [('score', '<f8')])
|
|
153
|
+
>>> sb.dtype.descr
|
|
154
|
+
[('id', '<i8'), ('name', '|S3'), ('score', '<f8')]
|
|
155
|
+
|
|
156
|
+
>>> numpy.all(sa['id'] == sb['id'])
|
|
157
|
+
True
|
|
158
|
+
>>> numpy.all(sa['name'] == sb['name'])
|
|
159
|
+
True
|
|
160
|
+
"""
|
|
161
|
+
if a.dtype.fields is None:
|
|
162
|
+
raise ValueError("'A' must be a structured numpy array")
|
|
163
|
+
b = np.empty(a.shape, dtype=a.dtype.descr + descr)
|
|
164
|
+
for name in a.dtype.names:
|
|
165
|
+
b[name] = a[name]
|
|
166
|
+
return b
|