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/decorator.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import subprocess
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def deprecated(alternative):
|
|
8
|
+
def decorator(func):
|
|
9
|
+
def new_func(*args, **kwargs):
|
|
10
|
+
# Raise a DeprecationWarning with the specified message.
|
|
11
|
+
message = f"Call to deprecated function {func.__name__}."
|
|
12
|
+
if alternative:
|
|
13
|
+
message += f" Use {alternative} instead."
|
|
14
|
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
|
15
|
+
return func(*args, **kwargs)
|
|
16
|
+
return new_func
|
|
17
|
+
return decorator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_contents(func):
|
|
21
|
+
"""
|
|
22
|
+
Decorator to handle gzip compression based on filename and pass its contents
|
|
23
|
+
to the function
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@wraps(func)
|
|
27
|
+
def wrapper(filename, *args, **kwargs):
|
|
28
|
+
|
|
29
|
+
doc = None
|
|
30
|
+
|
|
31
|
+
if filename.endswith('.gz'):
|
|
32
|
+
with gzip.open(filename, 'rt', encoding='utf-8') as fh:
|
|
33
|
+
doc = fh.read()
|
|
34
|
+
elif filename.endswith('.Z'):
|
|
35
|
+
result = subprocess.run(['uncompress', '-c', filename],
|
|
36
|
+
stdout=subprocess.PIPE,
|
|
37
|
+
stderr=subprocess.PIPE,
|
|
38
|
+
check=True,
|
|
39
|
+
text=True)
|
|
40
|
+
doc = result.stdout
|
|
41
|
+
else:
|
|
42
|
+
with open(filename, 'rt', encoding='utf-8') as fh:
|
|
43
|
+
doc = fh.read()
|
|
44
|
+
|
|
45
|
+
return func(doc, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
return wrapper
|
pygnss/file.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import IO
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def process_filename_or_file_handler(mode):
|
|
6
|
+
def decorator(func):
|
|
7
|
+
@wraps(func)
|
|
8
|
+
def wrapper(input, *args, **kwargs):
|
|
9
|
+
if isinstance(input, str):
|
|
10
|
+
with open(input, mode) as fh:
|
|
11
|
+
return func(fh, *args, **kwargs)
|
|
12
|
+
else:
|
|
13
|
+
return func(input, *args, **kwargs)
|
|
14
|
+
return wrapper
|
|
15
|
+
return decorator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def grep_lines(filename: str, pattern_string: str):
|
|
19
|
+
"""
|
|
20
|
+
Generator function used to grep lines from a file. Can be used in methods
|
|
21
|
+
such as numpy.genfromtxt, ...
|
|
22
|
+
|
|
23
|
+
>>> generator = grep_lines(filename, "pattern")
|
|
24
|
+
>>> data = numpy.loadtxt(generator)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
with open(filename, 'r') as fh:
|
|
28
|
+
for line in fh:
|
|
29
|
+
if pattern_string in line:
|
|
30
|
+
yield line
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def skip_lines(fh: IO, n_lines: int):
|
|
34
|
+
|
|
35
|
+
for _ in range(n_lines):
|
|
36
|
+
fh.readline()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for the filter class
|
|
3
|
+
|
|
4
|
+
Some notation conventions:
|
|
5
|
+
|
|
6
|
+
- $x_m$ Predicted state from the previous k-1 state
|
|
7
|
+
- $y_m$ indicates the observations resulted from the predicted
|
|
8
|
+
state ($x_m$)
|
|
9
|
+
- $H$ is the design (Jacobian) matrix, that translates from state to observation
|
|
10
|
+
(i.e. $y = H \\cdot x$)
|
|
11
|
+
- $\\Phi$ is the state transition matrix, that translates from the
|
|
12
|
+
state k-1 to the predicted state ($x_m$)
|
|
13
|
+
"""
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from collections import namedtuple
|
|
16
|
+
from typing import List
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
State = List[float] # e.g. np.array
|
|
21
|
+
|
|
22
|
+
ModelObs = namedtuple('ModelObs', ('y_m', 'H')) # y_m must be an array of arrays (2D shaped)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Model(ABC):
|
|
26
|
+
"""
|
|
27
|
+
Abstract class that declares the interface for entities that model
|
|
28
|
+
an entity to be used by an estimation filter
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def propagate_state(self, state: np.array) -> np.array:
|
|
33
|
+
"""
|
|
34
|
+
Propagate a state from time k-1 to k
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def to_observations(self, state: np.array, compute_jacobian: bool = False, **kwargs) -> ModelObs:
|
|
39
|
+
"""
|
|
40
|
+
Propagate a state to its corresponding modelled observations (i.e.
|
|
41
|
+
compute expected observations/measurements for the input state)
|
|
42
|
+
|
|
43
|
+
:return: a tuple where the first element are the observations and the second
|
|
44
|
+
is the Jacobian matrix (if compute_jacobian is True, otherwise the second
|
|
45
|
+
element will be None)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def Phi(self):
|
|
49
|
+
"""
|
|
50
|
+
Provide with the state transition matrix (also noted F in certain
|
|
51
|
+
Kalman notation)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class StateHandler(ABC):
|
|
56
|
+
"""
|
|
57
|
+
Abstract class that handles the state generated by UKF
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def process_state(self, state: np.array, covariance_matrix: np.array, **kwargs):
|
|
62
|
+
"""
|
|
63
|
+
Process the state and associated covariance_matrix
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FilterInterface(ABC):
|
|
68
|
+
"""Interface for the Filter class"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def process(self, y_k: np.array, R: np.array, **kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Process an observation batch
|
|
74
|
+
|
|
75
|
+
:param y_k: object that contains the observations
|
|
76
|
+
:param R: matrix with the covariance of the measurement (i.e. measurement noise)
|
|
77
|
+
"""
|
pygnss/filter/ekf.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for the EKF
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from . import StateHandler, Model, FilterInterface
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Ekf(FilterInterface):
|
|
13
|
+
"""
|
|
14
|
+
Extended Kalman Filter (EKF)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self,
|
|
18
|
+
x0: np.array,
|
|
19
|
+
P0: np.array,
|
|
20
|
+
Q: np.array,
|
|
21
|
+
model: Model,
|
|
22
|
+
state_handler: StateHandler,
|
|
23
|
+
logger: logging.Logger = logging):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the EKF filter object
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
self.x = x0
|
|
29
|
+
self.P = P0
|
|
30
|
+
self.Q = Q
|
|
31
|
+
|
|
32
|
+
self.model = model
|
|
33
|
+
self.state_handler = state_handler
|
|
34
|
+
|
|
35
|
+
self.logger = logger
|
|
36
|
+
|
|
37
|
+
self.L = len(self.x)
|
|
38
|
+
|
|
39
|
+
def process(self, y_k: np.array, R: np.array, **kwargs):
|
|
40
|
+
"""
|
|
41
|
+
Process an observation batch
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Time update ----------------------------------------------------------
|
|
45
|
+
x_m, P_m = self._time_update()
|
|
46
|
+
|
|
47
|
+
# Measurement update ---------------------------------------------------
|
|
48
|
+
y_m, H = self.model.to_observations(x_m, compute_jacobian=True, **kwargs)
|
|
49
|
+
|
|
50
|
+
P_yy = H @ P_m @ H.T + R
|
|
51
|
+
P_xy = P_m @ H.T
|
|
52
|
+
|
|
53
|
+
self.x = x_m
|
|
54
|
+
self.P = P_m
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
K = P_xy @ np.linalg.inv(P_yy) # Calculate Kalman gain
|
|
58
|
+
|
|
59
|
+
self.x = self.x + K @ (y_k - y_m) # Update state estimate
|
|
60
|
+
self.P = self.P - K @ H @ P_m # Update covariance estimate
|
|
61
|
+
|
|
62
|
+
except np.linalg.LinAlgError as e:
|
|
63
|
+
self.logger.warning(f'Unable to compute state, keeping previous one. Error: {e}')
|
|
64
|
+
|
|
65
|
+
# Compute postfit residuals
|
|
66
|
+
r = y_k - self.model.to_observations(self.x, **kwargs).y_m
|
|
67
|
+
|
|
68
|
+
self.state_handler.process_state(self.x, self.P, postfits=r, **kwargs)
|
|
69
|
+
|
|
70
|
+
def _time_update(self) -> Tuple[np.array, np.array]:
|
|
71
|
+
"""
|
|
72
|
+
Perform a time update step
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
Phi = self.model.Phi
|
|
76
|
+
|
|
77
|
+
x_m = self.model.propagate_state(self.x)
|
|
78
|
+
P_m = Phi @ self.P @ Phi.T + self.Q
|
|
79
|
+
|
|
80
|
+
return x_m, P_m
|
pygnss/filter/models.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from . import Model, ModelObs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RangePositioning2D(Model):
|
|
7
|
+
"""
|
|
8
|
+
Basic 2D range-based positioning model
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, Phi: np.array, nodes: np.array):
|
|
12
|
+
"""
|
|
13
|
+
Instantiate a RangePositioning2D
|
|
14
|
+
|
|
15
|
+
:param Phi: a 2 x 2 matrix that propagates the state from k-1 to k
|
|
16
|
+
:param nodes: list of nodes of the positioning system, from which the
|
|
17
|
+
range will be computed
|
|
18
|
+
"""
|
|
19
|
+
self._Phi = Phi
|
|
20
|
+
self.nodes = nodes
|
|
21
|
+
|
|
22
|
+
def propagate_state(self, state: np.array):
|
|
23
|
+
"""
|
|
24
|
+
Propagate the state from k-1 to k
|
|
25
|
+
|
|
26
|
+
>>> Phi = np.eye(2)
|
|
27
|
+
>>> nodes = np.array([[0, 0], [0, 10], [10, 0]])
|
|
28
|
+
>>> model = RangePositioning2D(Phi, nodes)
|
|
29
|
+
>>> state_m = np.array([1, 2])
|
|
30
|
+
>>> model.propagate_state(state_m)
|
|
31
|
+
array([1., 2.])
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
return np.dot(self._Phi, state)
|
|
35
|
+
|
|
36
|
+
def to_observations(self, state: np.array, compute_jacobian: bool = False) -> ModelObs:
|
|
37
|
+
"""
|
|
38
|
+
Convert the state into observations using a range based 2D positioning model
|
|
39
|
+
|
|
40
|
+
>>> Phi = np.eye(2)
|
|
41
|
+
>>> nodes = np.array([[0, 0], [0, 10], [10, 0]])
|
|
42
|
+
>>> model = RangePositioning2D(Phi, nodes)
|
|
43
|
+
>>> state_m = np.array([1, 2])
|
|
44
|
+
>>> model.to_observations(state_m)
|
|
45
|
+
ModelObs(y_m=array([2.23606798, 8.06225775, 9.21954446]), H=None)
|
|
46
|
+
|
|
47
|
+
>>> model.to_observations(state_m, compute_jacobian=True)
|
|
48
|
+
ModelObs(y_m=array([2.23606798, 8.06225775, 9.21954446]), H=array([[ 0.4472136 , 0.89442719],
|
|
49
|
+
[ 0.12403473, -0.99227788],
|
|
50
|
+
[-0.97618706, 0.21693046]]))
|
|
51
|
+
"""
|
|
52
|
+
rho = state - self.nodes
|
|
53
|
+
ranges = np.sqrt(np.sum(np.power(rho, 2), axis=1))
|
|
54
|
+
|
|
55
|
+
H = None
|
|
56
|
+
|
|
57
|
+
if compute_jacobian is True:
|
|
58
|
+
H = rho / ranges[:, np.newaxis]
|
|
59
|
+
# Return a ModelObs namedtuple for compatibility with the filter
|
|
60
|
+
# API which expects an object with a ``y_m`` attribute.
|
|
61
|
+
return ModelObs(ranges, H)
|
|
62
|
+
|
|
63
|
+
def Phi(self):
|
|
64
|
+
"""
|
|
65
|
+
Get the state transition matrix
|
|
66
|
+
|
|
67
|
+
>>> Phi = np.eye(2)
|
|
68
|
+
>>> nodes = np.array([[0, 0], [0, 10], [10, 0]])
|
|
69
|
+
>>> model = RangePositioning2D(Phi, nodes)
|
|
70
|
+
>>> model.Phi()
|
|
71
|
+
array([[1., 0.],
|
|
72
|
+
[0., 1.]])
|
|
73
|
+
"""
|
|
74
|
+
return self._Phi
|