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