bayesfilter 0.0.2__tar.gz
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.
- bayesfilter-0.0.2/PKG-INFO +140 -0
- bayesfilter-0.0.2/README.md +132 -0
- bayesfilter-0.0.2/bayesfilter/__init__.py +8 -0
- bayesfilter-0.0.2/bayesfilter/distributions.py +163 -0
- bayesfilter-0.0.2/bayesfilter/filtering.py +122 -0
- bayesfilter-0.0.2/bayesfilter/model.py +84 -0
- bayesfilter-0.0.2/bayesfilter/observation.py +87 -0
- bayesfilter-0.0.2/bayesfilter/smoothing.py +69 -0
- bayesfilter-0.0.2/bayesfilter/test_filtering_smoothing.py +87 -0
- bayesfilter-0.0.2/bayesfilter/unscented.py +106 -0
- bayesfilter-0.0.2/bayesfilter/utilities.py +11 -0
- bayesfilter-0.0.2/bayesfilter.egg-info/PKG-INFO +140 -0
- bayesfilter-0.0.2/bayesfilter.egg-info/SOURCES.txt +16 -0
- bayesfilter-0.0.2/bayesfilter.egg-info/dependency_links.txt +1 -0
- bayesfilter-0.0.2/bayesfilter.egg-info/requires.txt +1 -0
- bayesfilter-0.0.2/bayesfilter.egg-info/top_level.txt +1 -0
- bayesfilter-0.0.2/setup.cfg +4 -0
- bayesfilter-0.0.2/setup.py +15 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: bayesfilter
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Author: Hugo Hadfield
|
|
5
|
+
License: MIT
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
|
|
9
|
+
# BayesFilter
|
|
10
|
+
|
|
11
|
+
BayesFilter is a Python library for Bayesian filtering and smoothing. This library provides tools for implementing Bayesian filters, Rauch-Tung-Striebel smoothers, and other related methods. The only dependency is NumPy.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
To install BayesFilter, just use `pip`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install bayesfilter
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic Structure
|
|
24
|
+
|
|
25
|
+
The library consists of several modules, each responsible for different parts of the Bayesian filtering and smoothing process:
|
|
26
|
+
|
|
27
|
+
- `distributions.py`: Defines the distribution classes, including the Gaussian distribution used for the filters.
|
|
28
|
+
- `filtering.py`: Implements the BayesianFilter class, which runs the filtering process.
|
|
29
|
+
- `model.py`: Contains the StateTransitionModel class for state transitions.
|
|
30
|
+
- `observation.py`: Defines the Observation class for observation models.
|
|
31
|
+
- `smoothing.py`: Implements the RTS (Rauch-Tung-Striebel) smoother.
|
|
32
|
+
- `unscented.py`: Provides functions for the unscented transform.
|
|
33
|
+
- `utilities.py`: Contains utility functions used throughout the library.
|
|
34
|
+
- `test_filtering_smoothing.py`: Contains tests for filtering and smoothing.
|
|
35
|
+
|
|
36
|
+
### Example
|
|
37
|
+
|
|
38
|
+
Here is a basic example of how to set up and run a Bayesian filter with the provided library:
|
|
39
|
+
|
|
40
|
+
1. **Setup Functions**:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
def setup_functions():
|
|
44
|
+
def transition_func(x, delta_t_s):
|
|
45
|
+
return np.array([x[0]])
|
|
46
|
+
|
|
47
|
+
def transition_jacobian_func(x, delta_t_s):
|
|
48
|
+
return np.array([[1.0]])
|
|
49
|
+
|
|
50
|
+
def observation_func(x):
|
|
51
|
+
return np.array([np.sin(x[0]), np.cos(x[0])])
|
|
52
|
+
|
|
53
|
+
def observation_jacobian_func(x):
|
|
54
|
+
return np.array([[np.cos(x[0])], [-np.sin(x[0])]])
|
|
55
|
+
|
|
56
|
+
return transition_func, transition_jacobian_func, observation_func, observation_jacobian_func
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. **Setup Filter and Observations**:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
def setup_filter_and_observations():
|
|
63
|
+
rng = np.random.default_rng(0)
|
|
64
|
+
transition_func, transition_jacobian_func, observation_func, observation_jacobian_func = setup_functions()
|
|
65
|
+
|
|
66
|
+
transition_model = StateTransitionModel(
|
|
67
|
+
transition_func,
|
|
68
|
+
1e-8*np.eye(1),
|
|
69
|
+
transition_jacobian_func
|
|
70
|
+
)
|
|
71
|
+
initial_state = Gaussian(np.array([0.0]), np.eye(1))
|
|
72
|
+
filter = BayesianFilter(transition_model, initial_state)
|
|
73
|
+
|
|
74
|
+
true_state = np.array([-0.1])
|
|
75
|
+
noise_std = 0.2
|
|
76
|
+
observations = []
|
|
77
|
+
for theta in np.linspace(0, 2*np.pi, 1000):
|
|
78
|
+
observation = observation_func(true_state) + rng.normal(0, noise_std, 2)
|
|
79
|
+
observations.append(Observation(observation, noise_std*np.eye(2), observation_func, observation_jacobian_func))
|
|
80
|
+
return filter, observations, true_state
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. **Run Filter**:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def test_filter_noisy_sin(use_jacobian=True):
|
|
87
|
+
filter, observations, true_state = setup_filter_and_observations()
|
|
88
|
+
filter.run(observations, np.linspace(0, 2*np.pi, 1000), 100.0, use_jacobian=use_jacobian)
|
|
89
|
+
np.testing.assert_allclose(filter.state.mean(), true_state, atol=1e-2)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Tests
|
|
93
|
+
|
|
94
|
+
The library includes a set of tests to ensure the functionality of the filtering and smoothing algorithms. These can be run as follows:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python test_filtering_smoothing.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
### `distributions.py`
|
|
103
|
+
|
|
104
|
+
Defines the Gaussian distribution class used for state representation and propagation.
|
|
105
|
+
|
|
106
|
+
### `filtering.py`
|
|
107
|
+
|
|
108
|
+
Implements the `BayesianFilter` class, responsible for running the filtering process with predict and update steps.
|
|
109
|
+
|
|
110
|
+
### `model.py`
|
|
111
|
+
|
|
112
|
+
Contains the `StateTransitionModel` class, representing the state transition model.
|
|
113
|
+
|
|
114
|
+
### `observation.py`
|
|
115
|
+
|
|
116
|
+
Defines the `Observation` class, representing the observation model.
|
|
117
|
+
|
|
118
|
+
### `smoothing.py`
|
|
119
|
+
|
|
120
|
+
Implements the `RTS` class for Rauch-Tung-Striebel smoothing.
|
|
121
|
+
|
|
122
|
+
### `unscented.py`
|
|
123
|
+
|
|
124
|
+
Provides functions for the unscented transform, including `unscented_transform` and `propagate_gaussian`.
|
|
125
|
+
|
|
126
|
+
### `utilities.py`
|
|
127
|
+
|
|
128
|
+
Contains utility functions like `propagate_covariance`.
|
|
129
|
+
|
|
130
|
+
### `test_filtering_smoothing.py`
|
|
131
|
+
|
|
132
|
+
Includes tests for filtering and smoothing to validate the implementation.
|
|
133
|
+
|
|
134
|
+
## Author
|
|
135
|
+
|
|
136
|
+
Hugo Hadfield
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# BayesFilter
|
|
2
|
+
|
|
3
|
+
BayesFilter is a Python library for Bayesian filtering and smoothing. This library provides tools for implementing Bayesian filters, Rauch-Tung-Striebel smoothers, and other related methods. The only dependency is NumPy.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install BayesFilter, just use `pip`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install bayesfilter
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Basic Structure
|
|
16
|
+
|
|
17
|
+
The library consists of several modules, each responsible for different parts of the Bayesian filtering and smoothing process:
|
|
18
|
+
|
|
19
|
+
- `distributions.py`: Defines the distribution classes, including the Gaussian distribution used for the filters.
|
|
20
|
+
- `filtering.py`: Implements the BayesianFilter class, which runs the filtering process.
|
|
21
|
+
- `model.py`: Contains the StateTransitionModel class for state transitions.
|
|
22
|
+
- `observation.py`: Defines the Observation class for observation models.
|
|
23
|
+
- `smoothing.py`: Implements the RTS (Rauch-Tung-Striebel) smoother.
|
|
24
|
+
- `unscented.py`: Provides functions for the unscented transform.
|
|
25
|
+
- `utilities.py`: Contains utility functions used throughout the library.
|
|
26
|
+
- `test_filtering_smoothing.py`: Contains tests for filtering and smoothing.
|
|
27
|
+
|
|
28
|
+
### Example
|
|
29
|
+
|
|
30
|
+
Here is a basic example of how to set up and run a Bayesian filter with the provided library:
|
|
31
|
+
|
|
32
|
+
1. **Setup Functions**:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
def setup_functions():
|
|
36
|
+
def transition_func(x, delta_t_s):
|
|
37
|
+
return np.array([x[0]])
|
|
38
|
+
|
|
39
|
+
def transition_jacobian_func(x, delta_t_s):
|
|
40
|
+
return np.array([[1.0]])
|
|
41
|
+
|
|
42
|
+
def observation_func(x):
|
|
43
|
+
return np.array([np.sin(x[0]), np.cos(x[0])])
|
|
44
|
+
|
|
45
|
+
def observation_jacobian_func(x):
|
|
46
|
+
return np.array([[np.cos(x[0])], [-np.sin(x[0])]])
|
|
47
|
+
|
|
48
|
+
return transition_func, transition_jacobian_func, observation_func, observation_jacobian_func
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
2. **Setup Filter and Observations**:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def setup_filter_and_observations():
|
|
55
|
+
rng = np.random.default_rng(0)
|
|
56
|
+
transition_func, transition_jacobian_func, observation_func, observation_jacobian_func = setup_functions()
|
|
57
|
+
|
|
58
|
+
transition_model = StateTransitionModel(
|
|
59
|
+
transition_func,
|
|
60
|
+
1e-8*np.eye(1),
|
|
61
|
+
transition_jacobian_func
|
|
62
|
+
)
|
|
63
|
+
initial_state = Gaussian(np.array([0.0]), np.eye(1))
|
|
64
|
+
filter = BayesianFilter(transition_model, initial_state)
|
|
65
|
+
|
|
66
|
+
true_state = np.array([-0.1])
|
|
67
|
+
noise_std = 0.2
|
|
68
|
+
observations = []
|
|
69
|
+
for theta in np.linspace(0, 2*np.pi, 1000):
|
|
70
|
+
observation = observation_func(true_state) + rng.normal(0, noise_std, 2)
|
|
71
|
+
observations.append(Observation(observation, noise_std*np.eye(2), observation_func, observation_jacobian_func))
|
|
72
|
+
return filter, observations, true_state
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. **Run Filter**:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
def test_filter_noisy_sin(use_jacobian=True):
|
|
79
|
+
filter, observations, true_state = setup_filter_and_observations()
|
|
80
|
+
filter.run(observations, np.linspace(0, 2*np.pi, 1000), 100.0, use_jacobian=use_jacobian)
|
|
81
|
+
np.testing.assert_allclose(filter.state.mean(), true_state, atol=1e-2)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Tests
|
|
85
|
+
|
|
86
|
+
The library includes a set of tests to ensure the functionality of the filtering and smoothing algorithms. These can be run as follows:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python test_filtering_smoothing.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
### `distributions.py`
|
|
95
|
+
|
|
96
|
+
Defines the Gaussian distribution class used for state representation and propagation.
|
|
97
|
+
|
|
98
|
+
### `filtering.py`
|
|
99
|
+
|
|
100
|
+
Implements the `BayesianFilter` class, responsible for running the filtering process with predict and update steps.
|
|
101
|
+
|
|
102
|
+
### `model.py`
|
|
103
|
+
|
|
104
|
+
Contains the `StateTransitionModel` class, representing the state transition model.
|
|
105
|
+
|
|
106
|
+
### `observation.py`
|
|
107
|
+
|
|
108
|
+
Defines the `Observation` class, representing the observation model.
|
|
109
|
+
|
|
110
|
+
### `smoothing.py`
|
|
111
|
+
|
|
112
|
+
Implements the `RTS` class for Rauch-Tung-Striebel smoothing.
|
|
113
|
+
|
|
114
|
+
### `unscented.py`
|
|
115
|
+
|
|
116
|
+
Provides functions for the unscented transform, including `unscented_transform` and `propagate_gaussian`.
|
|
117
|
+
|
|
118
|
+
### `utilities.py`
|
|
119
|
+
|
|
120
|
+
Contains utility functions like `propagate_covariance`.
|
|
121
|
+
|
|
122
|
+
### `test_filtering_smoothing.py`
|
|
123
|
+
|
|
124
|
+
Includes tests for filtering and smoothing to validate the implementation.
|
|
125
|
+
|
|
126
|
+
## Author
|
|
127
|
+
|
|
128
|
+
Hugo Hadfield
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
DEFAULT_ALPHA = 1e-3
|
|
7
|
+
DEFAULT_BETA = 2.0
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_lambda(n: int, alpha: float = DEFAULT_ALPHA) -> int:
|
|
11
|
+
"""
|
|
12
|
+
Compute the lambda parameter for the unscented transform.
|
|
13
|
+
"""
|
|
14
|
+
kappa = 3 - n
|
|
15
|
+
l = alpha*alpha*(n + kappa) - n
|
|
16
|
+
return l
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_covariance_mean_weight(
|
|
20
|
+
dimension: int,
|
|
21
|
+
alpha: float = DEFAULT_ALPHA,
|
|
22
|
+
beta: float = DEFAULT_BETA
|
|
23
|
+
) -> np.ndarray:
|
|
24
|
+
"""
|
|
25
|
+
Compute the covariance weight for the sigma points representing the mean.
|
|
26
|
+
"""
|
|
27
|
+
lamb = compute_lambda(dimension)
|
|
28
|
+
return lamb / (dimension + lamb) + (1 - alpha*alpha + beta)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Distribution:
|
|
32
|
+
"""
|
|
33
|
+
Represents a generic probability distribution.
|
|
34
|
+
"""
|
|
35
|
+
def dimension(self):
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
def mean(self):
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
def covariance(self):
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def compute_sigma_points(self):
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
def sample(self):
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def from_samples(self, samples: List[np.ndarray]):
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Gaussian(Distribution):
|
|
58
|
+
"""
|
|
59
|
+
Represents a Gaussian distribution.
|
|
60
|
+
"""
|
|
61
|
+
def __init__(self, mean, covariance, rng=None):
|
|
62
|
+
self._mean = mean
|
|
63
|
+
self._covariance = covariance
|
|
64
|
+
self._rng = np.random.default_rng(rng)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def rng(self):
|
|
68
|
+
return self._rng
|
|
69
|
+
|
|
70
|
+
def mean(self) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Return the mean of the Gaussian distribution.
|
|
73
|
+
"""
|
|
74
|
+
return self._mean
|
|
75
|
+
|
|
76
|
+
def covariance(self) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Return the covariance of the Gaussian distribution.
|
|
79
|
+
"""
|
|
80
|
+
return self._covariance
|
|
81
|
+
|
|
82
|
+
def dimension(self) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Return the dimension of the Gaussian distribution.
|
|
85
|
+
"""
|
|
86
|
+
return len(self.mean())
|
|
87
|
+
|
|
88
|
+
def sqrt_covariance(self) -> np.ndarray:
|
|
89
|
+
"""
|
|
90
|
+
Return the square root of the covariance matrix.
|
|
91
|
+
"""
|
|
92
|
+
if self.dimension() == 1:
|
|
93
|
+
return np.array([[np.sqrt(self.covariance()[0, 0])]])
|
|
94
|
+
return np.linalg.cholesky(self.covariance())
|
|
95
|
+
|
|
96
|
+
def compute_sigma_points(self) -> np.ndarray:
|
|
97
|
+
"""
|
|
98
|
+
Compute the sigma points for the Gaussian distribution.
|
|
99
|
+
"""
|
|
100
|
+
mean = self.mean()
|
|
101
|
+
sqrt_covariance = self.sqrt_covariance()
|
|
102
|
+
n = self.dimension()
|
|
103
|
+
lamb = compute_lambda(n)
|
|
104
|
+
factor = np.sqrt(n + lamb)
|
|
105
|
+
sigma_points = np.zeros((2*n + 1, n))
|
|
106
|
+
sigma_points[0, :] = mean
|
|
107
|
+
counter = 1
|
|
108
|
+
for i in range(n): # 2n + 1 sigma points
|
|
109
|
+
sigma_points[counter, :] = mean + factor*sqrt_covariance[:, i]
|
|
110
|
+
counter += 1
|
|
111
|
+
sigma_points[counter, :] = mean - factor*sqrt_covariance[:, i]
|
|
112
|
+
counter += 1
|
|
113
|
+
return sigma_points
|
|
114
|
+
|
|
115
|
+
def compute_weights(self) -> np.ndarray:
|
|
116
|
+
"""
|
|
117
|
+
Compute the weights for the sigma points.
|
|
118
|
+
"""
|
|
119
|
+
n = self.dimension()
|
|
120
|
+
lamb = compute_lambda(n)
|
|
121
|
+
weights = np.zeros(2*n + 1)
|
|
122
|
+
# The first weight is for the mean
|
|
123
|
+
weights[0] = lamb / (n + lamb)
|
|
124
|
+
for i in range(2*n):
|
|
125
|
+
weights[i + 1] = 1.0 / (2.0*(n + lamb))
|
|
126
|
+
return weights
|
|
127
|
+
|
|
128
|
+
def from_sigma_points(self, sigma_points: np.ndarray, weights: np.ndarray):
|
|
129
|
+
"""
|
|
130
|
+
Create a Gaussian distribution from the given sigma points.
|
|
131
|
+
Equations coming from https://arxiv.org/pdf/2104.01958
|
|
132
|
+
"""
|
|
133
|
+
new_n = sigma_points.shape[1]
|
|
134
|
+
new_mean = np.zeros(new_n)
|
|
135
|
+
for i in range(sigma_points.shape[0]):
|
|
136
|
+
new_mean += weights[i] * sigma_points[i, :]
|
|
137
|
+
new_covariance = np.zeros((new_n, new_n))
|
|
138
|
+
for i in range(sigma_points.shape[0]):
|
|
139
|
+
diff = sigma_points[i, :] - new_mean
|
|
140
|
+
if i == 0:
|
|
141
|
+
new_covariance += compute_covariance_mean_weight(self.dimension()) * np.outer(diff, diff)
|
|
142
|
+
else:
|
|
143
|
+
new_covariance += weights[i] * np.outer(diff, diff)
|
|
144
|
+
return Gaussian(new_mean, new_covariance, self.rng)
|
|
145
|
+
|
|
146
|
+
def sample(self) -> np.ndarray:
|
|
147
|
+
"""
|
|
148
|
+
Sample from the Gaussian distribution.
|
|
149
|
+
"""
|
|
150
|
+
return self.rng.multivariate_normal(self.mean(), self.covariance())
|
|
151
|
+
|
|
152
|
+
def from_samples(self, samples: List[np.ndarray]):
|
|
153
|
+
"""
|
|
154
|
+
Create a Gaussian distribution from the given samples.
|
|
155
|
+
"""
|
|
156
|
+
sample_array = np.array(samples, dtype=np.float64)
|
|
157
|
+
new_mean = np.mean(sample_array, axis=0)
|
|
158
|
+
new_covariance = np.cov(sample_array, rowvar=False)
|
|
159
|
+
return Gaussian(new_mean, new_covariance, self.rng)
|
|
160
|
+
|
|
161
|
+
def __repr__(self):
|
|
162
|
+
return f'Gaussian(mean={self.mean()}, covariance={self.covariance()})'
|
|
163
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import tqdm
|
|
7
|
+
except ImportError:
|
|
8
|
+
tqdm = None
|
|
9
|
+
|
|
10
|
+
from bayesfilter.distributions import Gaussian
|
|
11
|
+
from bayesfilter.model import StateTransitionModel
|
|
12
|
+
from bayesfilter.observation import Observation
|
|
13
|
+
|
|
14
|
+
class BayesianFilter:
|
|
15
|
+
def __init__(self, state_transition_model: StateTransitionModel, initial_state: Gaussian):
|
|
16
|
+
"""
|
|
17
|
+
Set up the bayesian filter with the state transition model and initial state
|
|
18
|
+
"""
|
|
19
|
+
self.transition_model = state_transition_model
|
|
20
|
+
self.state = initial_state
|
|
21
|
+
|
|
22
|
+
def predict(self, state: Gaussian, delta_t_s: float, use_jacobian: bool = True) -> Gaussian:
|
|
23
|
+
"""
|
|
24
|
+
Predict the next state from a given state
|
|
25
|
+
"""
|
|
26
|
+
# Predict the next state
|
|
27
|
+
return self.transition_model.predict(state, delta_t_s, use_jacobian=use_jacobian)
|
|
28
|
+
|
|
29
|
+
def update(self, observation: Observation, predicted_state: Gaussian, use_jacobian: bool = False) -> Gaussian:
|
|
30
|
+
"""
|
|
31
|
+
Condition the state on a observation
|
|
32
|
+
"""
|
|
33
|
+
# Predict the observation (with noise)
|
|
34
|
+
predicted_obsurement, cross_covariance = observation.predict_with_cross_covariance(predicted_state, use_jacobian=use_jacobian)
|
|
35
|
+
|
|
36
|
+
# Compute the Kalman gain
|
|
37
|
+
kalman_gain = cross_covariance@np.linalg.inv(predicted_obsurement.noise_covariance)
|
|
38
|
+
|
|
39
|
+
# Compute the residual
|
|
40
|
+
residual = observation.observation - observation.observation_func(predicted_state.mean())
|
|
41
|
+
|
|
42
|
+
# Compute the new state
|
|
43
|
+
new_state = Gaussian(
|
|
44
|
+
mean=predicted_state.mean() + kalman_gain@residual,
|
|
45
|
+
covariance=predicted_state.covariance() - kalman_gain@predicted_obsurement.noise_covariance@kalman_gain.T
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Update the state
|
|
49
|
+
self.state = new_state
|
|
50
|
+
return new_state
|
|
51
|
+
|
|
52
|
+
def set_state(self, state: Gaussian):
|
|
53
|
+
"""
|
|
54
|
+
Directly set the filter state
|
|
55
|
+
"""
|
|
56
|
+
self.state = state
|
|
57
|
+
|
|
58
|
+
def run_synchronous(self, observations: List[Observation], times_s: List[float], use_jacobian=False) -> List[Gaussian]:
|
|
59
|
+
"""
|
|
60
|
+
Run the BayesianFilter on a list of observations.
|
|
61
|
+
Assumes that the observations are at a roughly fixed time interval.
|
|
62
|
+
use_jacobian allows you to switch between an extended and unscented prediction and update
|
|
63
|
+
"""
|
|
64
|
+
output_states = [self.state]
|
|
65
|
+
for i, observation in enumerate(observations[:len(times_s)-1]):
|
|
66
|
+
delta_t_s = times_s[i+1] - times_s[i]
|
|
67
|
+
predicted_state = self.predict(self.state, delta_t_s, use_jacobian=use_jacobian)
|
|
68
|
+
self.update(observation, predicted_state, use_jacobian=use_jacobian)
|
|
69
|
+
output_states.append(self.state)
|
|
70
|
+
return output_states
|
|
71
|
+
|
|
72
|
+
def run(
|
|
73
|
+
self,
|
|
74
|
+
observations: List[Observation],
|
|
75
|
+
times_s: List[float],
|
|
76
|
+
rate_hz: float,
|
|
77
|
+
use_jacobian=False
|
|
78
|
+
) -> Tuple[List[Gaussian], List[float]]:
|
|
79
|
+
"""
|
|
80
|
+
Run the bayesian filter on a list of observations at a fixed rate, start at the start of times_s and end at the
|
|
81
|
+
end of times_s. times_s referres to the observation times. use_jacobian allows you to switch
|
|
82
|
+
between an extended linearisation and an unscented transform for the prediction
|
|
83
|
+
"""
|
|
84
|
+
filter_states = []
|
|
85
|
+
filter_times = []
|
|
86
|
+
start_time = times_s[0]
|
|
87
|
+
end_time = times_s[-1]
|
|
88
|
+
current_obs_index = 0
|
|
89
|
+
# Step through the times from start to finish
|
|
90
|
+
# Use tqdm to keep track of progress if available
|
|
91
|
+
print(f"Running BayesianFilter at {rate_hz} Hz with use_jacobian: {use_jacobian}", flush=True)
|
|
92
|
+
delta_t_s = 1.0/rate_hz
|
|
93
|
+
iterator = np.arange(start_time, end_time + delta_t_s, delta_t_s)
|
|
94
|
+
if tqdm is not None:
|
|
95
|
+
iterator = tqdm.tqdm(iterator)
|
|
96
|
+
for current_time in iterator:
|
|
97
|
+
# Gets a list of all observations that have occured between previous time and curent_time
|
|
98
|
+
current_obs = []
|
|
99
|
+
if current_obs_index < len(times_s):
|
|
100
|
+
next_obs_time = times_s[current_obs_index]
|
|
101
|
+
while next_obs_time <= current_time:
|
|
102
|
+
current_obs.append(observations[current_obs_index])
|
|
103
|
+
current_obs_index += 1
|
|
104
|
+
if current_obs_index >= len(times_s):
|
|
105
|
+
break
|
|
106
|
+
next_obs_time = times_s[current_obs_index]
|
|
107
|
+
|
|
108
|
+
# Predict the next state
|
|
109
|
+
predicted_state = self.predict(self.state, delta_t_s, use_jacobian=use_jacobian)
|
|
110
|
+
|
|
111
|
+
# Update the state with the observations, if we have any
|
|
112
|
+
for observation in current_obs:
|
|
113
|
+
predicted_state = self.update(observation, predicted_state, use_jacobian=use_jacobian)
|
|
114
|
+
|
|
115
|
+
# Set the state
|
|
116
|
+
self.set_state(predicted_state)
|
|
117
|
+
|
|
118
|
+
# Append the state
|
|
119
|
+
filter_states.append(self.state)
|
|
120
|
+
filter_times.append(current_time)
|
|
121
|
+
|
|
122
|
+
return filter_states, filter_times
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from bayesfilter.distributions import Gaussian
|
|
7
|
+
from bayesfilter.utilities import propagate_covariance
|
|
8
|
+
from bayesfilter.unscented import (
|
|
9
|
+
propagate_gaussian, propagate_gaussian_cross_cov
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StateTransitionModel:
|
|
14
|
+
"""
|
|
15
|
+
Represents a state transition model
|
|
16
|
+
"""
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
transition_func: Callable[[np.ndarray, float], np.ndarray],
|
|
20
|
+
transition_noise_covariance: np.ndarray,
|
|
21
|
+
transition_jacobian_func: Optional[Callable[[np.ndarray, float], np.ndarray]] = None
|
|
22
|
+
):
|
|
23
|
+
self.transition_func = transition_func
|
|
24
|
+
self.dimension = len(transition_noise_covariance)
|
|
25
|
+
self.transition_noise_covariance = transition_noise_covariance
|
|
26
|
+
self.transition_jacobian_func = transition_jacobian_func
|
|
27
|
+
|
|
28
|
+
def predict_no_noise_jacobian(self, state: Gaussian, delta_t_s: float):
|
|
29
|
+
"""
|
|
30
|
+
Predict the next state without adding transition noise, uses the Jacobian of the
|
|
31
|
+
state transition function
|
|
32
|
+
"""
|
|
33
|
+
new_state = Gaussian(
|
|
34
|
+
mean=self.transition_func(state.mean(), delta_t_s),
|
|
35
|
+
covariance=propagate_covariance(
|
|
36
|
+
jacobian=self.get_jacobian(state, delta_t_s),
|
|
37
|
+
covariance=state.covariance()
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return new_state
|
|
41
|
+
|
|
42
|
+
def predict_no_noise(self, state: Gaussian, delta_t_s: float, use_jacobian: bool = False):
|
|
43
|
+
"""
|
|
44
|
+
Predict the next state without adding transition noise, uses the unscented transform
|
|
45
|
+
"""
|
|
46
|
+
if use_jacobian:
|
|
47
|
+
return self.predict_no_noise_jacobian(state, delta_t_s)
|
|
48
|
+
# Unscented transform
|
|
49
|
+
new_mean, new_covariance = propagate_gaussian(
|
|
50
|
+
state.mean(),
|
|
51
|
+
state.covariance(),
|
|
52
|
+
lambda x: self.transition_func(x, delta_t_s)
|
|
53
|
+
)
|
|
54
|
+
return Gaussian(new_mean, new_covariance)
|
|
55
|
+
|
|
56
|
+
def predict(self, state: Gaussian, delta_t_s: float, use_jacobian: bool = False):
|
|
57
|
+
"""
|
|
58
|
+
Predict the next state
|
|
59
|
+
"""
|
|
60
|
+
new_state = self.predict_no_noise(state, delta_t_s, use_jacobian)
|
|
61
|
+
return Gaussian(new_state.mean(), new_state.covariance() + self.transition_noise_covariance)
|
|
62
|
+
|
|
63
|
+
def predict_with_cross_covariance(self, state: Gaussian, delta_t_s: float, use_jacobian=False):
|
|
64
|
+
"""
|
|
65
|
+
Predict the next state and return the cross-covariance too
|
|
66
|
+
"""
|
|
67
|
+
if use_jacobian:
|
|
68
|
+
pred_mean = self.transition_func(state.mean(), delta_t_s)
|
|
69
|
+
transition_func_jac = self.get_jacobian(state, delta_t_s)
|
|
70
|
+
pred_cov = propagate_covariance(transition_func_jac, state.covariance())
|
|
71
|
+
cross_cov = pred_cov@transition_func_jac.T
|
|
72
|
+
return Gaussian(pred_mean, pred_cov + self.transition_noise_covariance), cross_cov
|
|
73
|
+
pred_mean, pred_cov, cross_cov = propagate_gaussian_cross_cov(
|
|
74
|
+
state.mean(),
|
|
75
|
+
state.covariance(),
|
|
76
|
+
lambda x: self.transition_func(x, delta_t_s)
|
|
77
|
+
)
|
|
78
|
+
return Gaussian(pred_mean, pred_cov + self.transition_noise_covariance), cross_cov
|
|
79
|
+
|
|
80
|
+
def get_jacobian(self, state: Gaussian, delta_t_s: float):
|
|
81
|
+
if self.transition_jacobian_func is None:
|
|
82
|
+
raise ValueError("Transition Jacobian function not provided")
|
|
83
|
+
return self.transition_jacobian_func(state.mean(), delta_t_s)
|
|
84
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from bayesfilter.distributions import Gaussian
|
|
7
|
+
from bayesfilter.utilities import propagate_covariance
|
|
8
|
+
from bayesfilter.unscented import (
|
|
9
|
+
propagate_gaussian, propagate_gaussian_cross_cov
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
class Observation:
|
|
13
|
+
"""
|
|
14
|
+
Represents an observation model
|
|
15
|
+
"""
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
observation: np.ndarray,
|
|
19
|
+
noise_covariance: np.ndarray,
|
|
20
|
+
observation_func: Callable[[np.ndarray], np.ndarray],
|
|
21
|
+
jacobian_func: Optional[Callable[[np.ndarray], np.ndarray]] = None
|
|
22
|
+
):
|
|
23
|
+
self.observation = observation.copy()
|
|
24
|
+
self.noise_covariance = noise_covariance.copy()
|
|
25
|
+
self.observation_func = observation_func
|
|
26
|
+
self.dimension = len(noise_covariance)
|
|
27
|
+
self.jacobian_func = jacobian_func
|
|
28
|
+
|
|
29
|
+
def predict_no_noise_jacobian(self, state: Gaussian):
|
|
30
|
+
"""
|
|
31
|
+
Predict an observation from a state without adding observation noise
|
|
32
|
+
"""
|
|
33
|
+
# Predict observation
|
|
34
|
+
pred_obs = self.observation_func(state.mean())
|
|
35
|
+
# Predict covariance
|
|
36
|
+
jac = self.get_jacobian(state.mean())
|
|
37
|
+
pred_cov = propagate_covariance(
|
|
38
|
+
jacobian=jac,
|
|
39
|
+
covariance=state.covariance()
|
|
40
|
+
)
|
|
41
|
+
return Observation(pred_obs, pred_cov, self.observation_func)
|
|
42
|
+
|
|
43
|
+
def predict_no_noise(self, state: Gaussian, use_jacobian: bool = False):
|
|
44
|
+
"""
|
|
45
|
+
Predict an observation from a state without adding observation noise
|
|
46
|
+
"""
|
|
47
|
+
if use_jacobian:
|
|
48
|
+
return self.predict_no_noise_jacobian(state)
|
|
49
|
+
# Unscented transform
|
|
50
|
+
pred_mean, pred_cov = propagate_gaussian(
|
|
51
|
+
state.mean(),
|
|
52
|
+
state.covariance(),
|
|
53
|
+
self.observation_func
|
|
54
|
+
)
|
|
55
|
+
return Observation(pred_mean, pred_cov, self.observation_func)
|
|
56
|
+
|
|
57
|
+
def predict(self, state: Gaussian, use_jacobian: bool = False):
|
|
58
|
+
"""
|
|
59
|
+
Predict an observation from a state
|
|
60
|
+
"""
|
|
61
|
+
observation = self.predict_no_noise(state, use_jacobian)
|
|
62
|
+
observation.noise_covariance += self.noise_covariance
|
|
63
|
+
return observation
|
|
64
|
+
|
|
65
|
+
def predict_with_cross_covariance(self, state: Gaussian, use_jacobian: bool = False):
|
|
66
|
+
"""
|
|
67
|
+
Predict an observation from a state and also return the cross-covariance
|
|
68
|
+
"""
|
|
69
|
+
if use_jacobian:
|
|
70
|
+
predicted_obs = self.observation_func(state.mean())
|
|
71
|
+
observation_jacobian = self.get_jacobian(state.mean())
|
|
72
|
+
pred_cov = propagate_covariance(observation_jacobian, state.covariance())
|
|
73
|
+
cross_covariance = state.covariance()@observation_jacobian.T
|
|
74
|
+
return Observation(predicted_obs, pred_cov + self.noise_covariance, self.observation_func), cross_covariance
|
|
75
|
+
|
|
76
|
+
pred_mean, pred_cov, cross_cov = propagate_gaussian_cross_cov(
|
|
77
|
+
state.mean(),
|
|
78
|
+
state.covariance(),
|
|
79
|
+
self.observation_func
|
|
80
|
+
)
|
|
81
|
+
return Observation(pred_mean, pred_cov + self.noise_covariance, self.observation_func), cross_cov
|
|
82
|
+
|
|
83
|
+
def get_jacobian(self, state: np.ndarray):
|
|
84
|
+
if self.jacobian_func is None:
|
|
85
|
+
raise ValueError("Observation Jacobian function not provided")
|
|
86
|
+
return self.jacobian_func(state)
|
|
87
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from bayesfilter.distributions import Gaussian
|
|
6
|
+
from bayesfilter.observation import Observation
|
|
7
|
+
from bayesfilter.filtering import BayesianFilter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RTS:
|
|
11
|
+
"""
|
|
12
|
+
General Rauch-Tung-Striebel smoother.
|
|
13
|
+
Equations from BAYESIAN FILTERING AND SMOOTHING by Simo Sarkka, what an absolute banger of a textbook.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, filter: BayesianFilter):
|
|
16
|
+
self.filter = filter
|
|
17
|
+
|
|
18
|
+
def apply(self, filter_states: List[Gaussian], time_list_s: List[float], use_jacobian=False):
|
|
19
|
+
smoother_states = []
|
|
20
|
+
# We start the smoother with the last filter state
|
|
21
|
+
current_covariance = filter_states[-1].covariance()
|
|
22
|
+
current_mean = filter_states[-1].mean()
|
|
23
|
+
current_mean_s = current_mean
|
|
24
|
+
current_covariance_s = current_covariance
|
|
25
|
+
smoother_states.append(Gaussian(current_mean_s, current_covariance_s))
|
|
26
|
+
delta_t_s = time_list_s[-1] - time_list_s[-2]
|
|
27
|
+
|
|
28
|
+
# We iterate backwards through the states
|
|
29
|
+
for i in range(len(filter_states)-2, -1, -1):
|
|
30
|
+
# Get the data
|
|
31
|
+
current_state_object = filter_states[i]
|
|
32
|
+
current_mean = current_state_object.mean()
|
|
33
|
+
current_covariance = current_state_object.covariance()
|
|
34
|
+
delta_t_s = time_list_s[i+1] - time_list_s[i]
|
|
35
|
+
|
|
36
|
+
# Predict
|
|
37
|
+
pred_state, cross_covariance = self.filter.transition_model.predict_with_cross_covariance(
|
|
38
|
+
current_state_object, delta_t_s, use_jacobian=use_jacobian
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Calculate the smoother gain
|
|
42
|
+
G_k = cross_covariance@np.linalg.inv(pred_state.covariance())
|
|
43
|
+
|
|
44
|
+
# Calculate the new mean and covariance
|
|
45
|
+
smoothed_state = current_mean + G_k@(current_mean_s - pred_state.mean())
|
|
46
|
+
smoothed_covariance = current_covariance + G_k@(current_covariance_s - pred_state.covariance())@G_k.T
|
|
47
|
+
|
|
48
|
+
# Now update the current state
|
|
49
|
+
current_mean_s = smoothed_state
|
|
50
|
+
current_covariance_s = smoothed_covariance
|
|
51
|
+
|
|
52
|
+
# Append the state
|
|
53
|
+
smoother_states.append(Gaussian(smoothed_state, smoothed_covariance))
|
|
54
|
+
|
|
55
|
+
smoother_states.reverse()
|
|
56
|
+
return smoother_states
|
|
57
|
+
|
|
58
|
+
def smooth(
|
|
59
|
+
self,
|
|
60
|
+
observations: List[Observation],
|
|
61
|
+
times_s: List[float],
|
|
62
|
+
rate_hz: float = 1.0,
|
|
63
|
+
use_jacobian = False,
|
|
64
|
+
) -> Tuple[List[Gaussian], List[float]]:
|
|
65
|
+
"""
|
|
66
|
+
Smooths the observations using the general RTS algorithm
|
|
67
|
+
"""
|
|
68
|
+
filter_states, filter_times = self.filter.run(observations, times_s, rate_hz, use_jacobian=use_jacobian)
|
|
69
|
+
return self.apply(filter_states, filter_times, use_jacobian=use_jacobian)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from bayesfilter.distributions import Gaussian
|
|
5
|
+
from bayesfilter.filtering import BayesianFilter
|
|
6
|
+
from bayesfilter.observation import Observation
|
|
7
|
+
from bayesfilter.model import StateTransitionModel
|
|
8
|
+
from bayesfilter.smoothing import RTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_functions():
|
|
12
|
+
|
|
13
|
+
def transition_func(x, delta_t_s):
|
|
14
|
+
return np.array([x[0]])
|
|
15
|
+
|
|
16
|
+
def transition_jacobian_func(x, delta_t_s):
|
|
17
|
+
return np.array([[1.0]])
|
|
18
|
+
|
|
19
|
+
def observation_func(x):
|
|
20
|
+
return np.array([np.sin(x[0]), np.cos(x[0])])
|
|
21
|
+
|
|
22
|
+
def observation_jacobian_func(x):
|
|
23
|
+
return np.array([[np.cos(x[0])], [-np.sin(x[0])]])
|
|
24
|
+
|
|
25
|
+
return transition_func, transition_jacobian_func, observation_func, observation_jacobian_func
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_filter_and_observations():
|
|
29
|
+
# Set the random seed
|
|
30
|
+
rng = np.random.default_rng(0)
|
|
31
|
+
|
|
32
|
+
# Set up the functions
|
|
33
|
+
transition_func, transition_jacobian_func, observation_func, observation_jacobian_func = setup_functions()
|
|
34
|
+
|
|
35
|
+
# Set up the filter
|
|
36
|
+
transition_model = StateTransitionModel(
|
|
37
|
+
transition_func,
|
|
38
|
+
1e-8*np.eye(1),
|
|
39
|
+
transition_jacobian_func
|
|
40
|
+
)
|
|
41
|
+
initial_state = Gaussian(np.array([0.0]), np.eye(1))
|
|
42
|
+
filter = BayesianFilter(transition_model, initial_state)
|
|
43
|
+
|
|
44
|
+
# Set up the observations
|
|
45
|
+
true_state = np.array([-0.1])
|
|
46
|
+
noise_std = 0.2
|
|
47
|
+
observations = []
|
|
48
|
+
for theta in np.linspace(0, 2*np.pi, 1000):
|
|
49
|
+
observation = observation_func(true_state) + rng.normal(0, noise_std, 2)
|
|
50
|
+
observations.append(Observation(observation, noise_std*np.eye(2), observation_func, observation_jacobian_func))
|
|
51
|
+
return filter, observations, true_state
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_filter_noisy_sin(use_jacobian=True):
|
|
55
|
+
filter, observations, true_state = setup_filter_and_observations()
|
|
56
|
+
|
|
57
|
+
filter.run(observations, np.linspace(0, 2*np.pi, 1000), 100.0, use_jacobian=use_jacobian)
|
|
58
|
+
np.testing.assert_allclose(filter.state.mean(), true_state, atol=1e-2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_smoother_noisy_sin(enable_debug_plots=False, use_jacobian=True):
|
|
62
|
+
filter, observations, true_state = setup_filter_and_observations()
|
|
63
|
+
|
|
64
|
+
filter_states, filter_times = filter.run(observations, np.linspace(0, 2*np.pi, 1000), 100.0, use_jacobian=use_jacobian)
|
|
65
|
+
smoother = RTS(filter)
|
|
66
|
+
smoother_states = smoother.apply(filter_states, np.linspace(0, 2*np.pi, 1000), use_jacobian=use_jacobian)
|
|
67
|
+
assert len(smoother_states) == len(filter_states)
|
|
68
|
+
assert len(smoother_states) == len(filter_times)
|
|
69
|
+
|
|
70
|
+
if enable_debug_plots:
|
|
71
|
+
import matplotlib.pyplot as plt
|
|
72
|
+
plt.plot(filter_times, [state.mean()[0] for state in filter_states], label=f"{'Extended' if use_jacobian else 'Unscented'} Filter")
|
|
73
|
+
plt.plot(filter_times, [state.mean()[0] for state in smoother_states], label=f"{'Extended' if use_jacobian else 'Unscented'} Smoother")
|
|
74
|
+
plt.plot(filter_times, [true_state[0] for _ in filter_times], label="True value")
|
|
75
|
+
plt.legend()
|
|
76
|
+
plt.show()
|
|
77
|
+
|
|
78
|
+
np.testing.assert_allclose(smoother_states[0].mean(), true_state, atol=1e-2)
|
|
79
|
+
np.testing.assert_allclose(smoother_states[-1].mean(), true_state, atol=1e-2)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
test_filter_noisy_sin(True)
|
|
84
|
+
test_filter_noisy_sin(False)
|
|
85
|
+
test_smoother_noisy_sin(False, True)
|
|
86
|
+
test_smoother_noisy_sin(False, False)
|
|
87
|
+
print("All tests passed")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import Callable, List, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from bayesfilter.distributions import Distribution, Gaussian, compute_covariance_mean_weight
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unscented_transform(
|
|
9
|
+
distribution: Distribution,
|
|
10
|
+
non_linear_function: Callable,
|
|
11
|
+
) -> Distribution:
|
|
12
|
+
"""
|
|
13
|
+
Propagate the distribution through the non-linear function using the unscented transform.
|
|
14
|
+
"""
|
|
15
|
+
sigma_points = distribution.compute_sigma_points()
|
|
16
|
+
transformed_sigma_points = np.array([non_linear_function(sp) for sp in sigma_points])
|
|
17
|
+
weights = distribution.compute_weights()
|
|
18
|
+
return distribution.from_sigma_points(transformed_sigma_points, weights)
|
|
19
|
+
|
|
20
|
+
def unscented_transform_cross_cov(
|
|
21
|
+
distribution: Distribution,
|
|
22
|
+
non_linear_function: Callable,
|
|
23
|
+
) -> Tuple[Distribution, np.ndarray]:
|
|
24
|
+
"""
|
|
25
|
+
Propagate the distribution through the non-linear function using the unscented transform.
|
|
26
|
+
Return the cross-covariance between the mean and the transformed sigma points.
|
|
27
|
+
"""
|
|
28
|
+
sigma_points = distribution.compute_sigma_points()
|
|
29
|
+
transformed_sigma_points = np.array([non_linear_function(sp) for sp in sigma_points])
|
|
30
|
+
weights = distribution.compute_weights()
|
|
31
|
+
transformed_distribution = distribution.from_sigma_points(transformed_sigma_points, weights)
|
|
32
|
+
# Compute the cross-covariance
|
|
33
|
+
cross_covariance = np.zeros((distribution.dimension(), transformed_sigma_points.shape[1]))
|
|
34
|
+
for i in range(sigma_points.shape[0]):
|
|
35
|
+
diff_x = sigma_points[i, :] - distribution.mean()
|
|
36
|
+
diff_y = transformed_sigma_points[i, :] - transformed_distribution.mean()
|
|
37
|
+
if i == 0:
|
|
38
|
+
cross_covariance += compute_covariance_mean_weight(distribution.dimension()) * np.outer(diff_x, diff_y)
|
|
39
|
+
else:
|
|
40
|
+
cross_covariance += weights[i] * np.outer(diff_x, diff_y)
|
|
41
|
+
return transformed_distribution, cross_covariance
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def propagate_samples(
|
|
45
|
+
distribution: Distribution,
|
|
46
|
+
non_linear_function: Callable,
|
|
47
|
+
num_samples: int,
|
|
48
|
+
) -> Distribution:
|
|
49
|
+
"""
|
|
50
|
+
Sample a distribution and propagate the samples through the non-linear function.
|
|
51
|
+
Reestimate the distribution from the transformed samples.
|
|
52
|
+
"""
|
|
53
|
+
samples = [distribution.sample() for _ in range(num_samples)]
|
|
54
|
+
transformed_samples = [non_linear_function(sample) for sample in samples]
|
|
55
|
+
return distribution.from_samples(transformed_samples)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def propagate_gaussian(
|
|
59
|
+
mean: np.ndarray,
|
|
60
|
+
covariance: np.ndarray,
|
|
61
|
+
non_linear_function: Callable,
|
|
62
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
63
|
+
"""
|
|
64
|
+
Propagate a Gaussian through a function with the unscented transform.
|
|
65
|
+
"""
|
|
66
|
+
gaussian = Gaussian(mean, covariance)
|
|
67
|
+
new_gaussian = unscented_transform(gaussian, non_linear_function)
|
|
68
|
+
return new_gaussian.mean(), new_gaussian.covariance()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def propagate_gaussian_cross_cov(
|
|
72
|
+
mean: np.ndarray,
|
|
73
|
+
covariance: np.ndarray,
|
|
74
|
+
non_linear_function: Callable
|
|
75
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
76
|
+
"""
|
|
77
|
+
Propagate a Gaussian through a function with the unscented transform.
|
|
78
|
+
Return the cross-covariance between the mean and the transformed sigma points.
|
|
79
|
+
"""
|
|
80
|
+
gaussian = Gaussian(mean, covariance)
|
|
81
|
+
new_gaussian, cross_covariance = unscented_transform_cross_cov(gaussian, non_linear_function)
|
|
82
|
+
return new_gaussian.mean(), new_gaussian.covariance(), cross_covariance
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_unscented_transform_linear_func():
|
|
86
|
+
"""
|
|
87
|
+
Test the unscented transform with a linear function.
|
|
88
|
+
"""
|
|
89
|
+
mean = np.array([1.0, 2.0])
|
|
90
|
+
covariance = np.array([[1.0, 0.0], [0.0, 1.0]])
|
|
91
|
+
gaussian = Gaussian(mean, covariance)
|
|
92
|
+
|
|
93
|
+
def linear_function(x):
|
|
94
|
+
return np.array([2.0*x[0], 3.0*x[1]])
|
|
95
|
+
|
|
96
|
+
jacobian = np.array([[2.0, 0.0], [0.0, 3.0]])
|
|
97
|
+
|
|
98
|
+
transformed_gaussian = unscented_transform(gaussian, linear_function)
|
|
99
|
+
expected_mean = linear_function(mean)
|
|
100
|
+
expected_covariance = jacobian @ covariance @ jacobian.T
|
|
101
|
+
assert np.allclose(transformed_gaussian.mean(), expected_mean)
|
|
102
|
+
assert np.allclose(transformed_gaussian.covariance(), expected_covariance)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == '__main__':
|
|
106
|
+
test_unscented_transform_linear_func()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: bayesfilter
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Author: Hugo Hadfield
|
|
5
|
+
License: MIT
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
|
|
9
|
+
# BayesFilter
|
|
10
|
+
|
|
11
|
+
BayesFilter is a Python library for Bayesian filtering and smoothing. This library provides tools for implementing Bayesian filters, Rauch-Tung-Striebel smoothers, and other related methods. The only dependency is NumPy.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
To install BayesFilter, just use `pip`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install bayesfilter
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic Structure
|
|
24
|
+
|
|
25
|
+
The library consists of several modules, each responsible for different parts of the Bayesian filtering and smoothing process:
|
|
26
|
+
|
|
27
|
+
- `distributions.py`: Defines the distribution classes, including the Gaussian distribution used for the filters.
|
|
28
|
+
- `filtering.py`: Implements the BayesianFilter class, which runs the filtering process.
|
|
29
|
+
- `model.py`: Contains the StateTransitionModel class for state transitions.
|
|
30
|
+
- `observation.py`: Defines the Observation class for observation models.
|
|
31
|
+
- `smoothing.py`: Implements the RTS (Rauch-Tung-Striebel) smoother.
|
|
32
|
+
- `unscented.py`: Provides functions for the unscented transform.
|
|
33
|
+
- `utilities.py`: Contains utility functions used throughout the library.
|
|
34
|
+
- `test_filtering_smoothing.py`: Contains tests for filtering and smoothing.
|
|
35
|
+
|
|
36
|
+
### Example
|
|
37
|
+
|
|
38
|
+
Here is a basic example of how to set up and run a Bayesian filter with the provided library:
|
|
39
|
+
|
|
40
|
+
1. **Setup Functions**:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
def setup_functions():
|
|
44
|
+
def transition_func(x, delta_t_s):
|
|
45
|
+
return np.array([x[0]])
|
|
46
|
+
|
|
47
|
+
def transition_jacobian_func(x, delta_t_s):
|
|
48
|
+
return np.array([[1.0]])
|
|
49
|
+
|
|
50
|
+
def observation_func(x):
|
|
51
|
+
return np.array([np.sin(x[0]), np.cos(x[0])])
|
|
52
|
+
|
|
53
|
+
def observation_jacobian_func(x):
|
|
54
|
+
return np.array([[np.cos(x[0])], [-np.sin(x[0])]])
|
|
55
|
+
|
|
56
|
+
return transition_func, transition_jacobian_func, observation_func, observation_jacobian_func
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. **Setup Filter and Observations**:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
def setup_filter_and_observations():
|
|
63
|
+
rng = np.random.default_rng(0)
|
|
64
|
+
transition_func, transition_jacobian_func, observation_func, observation_jacobian_func = setup_functions()
|
|
65
|
+
|
|
66
|
+
transition_model = StateTransitionModel(
|
|
67
|
+
transition_func,
|
|
68
|
+
1e-8*np.eye(1),
|
|
69
|
+
transition_jacobian_func
|
|
70
|
+
)
|
|
71
|
+
initial_state = Gaussian(np.array([0.0]), np.eye(1))
|
|
72
|
+
filter = BayesianFilter(transition_model, initial_state)
|
|
73
|
+
|
|
74
|
+
true_state = np.array([-0.1])
|
|
75
|
+
noise_std = 0.2
|
|
76
|
+
observations = []
|
|
77
|
+
for theta in np.linspace(0, 2*np.pi, 1000):
|
|
78
|
+
observation = observation_func(true_state) + rng.normal(0, noise_std, 2)
|
|
79
|
+
observations.append(Observation(observation, noise_std*np.eye(2), observation_func, observation_jacobian_func))
|
|
80
|
+
return filter, observations, true_state
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. **Run Filter**:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def test_filter_noisy_sin(use_jacobian=True):
|
|
87
|
+
filter, observations, true_state = setup_filter_and_observations()
|
|
88
|
+
filter.run(observations, np.linspace(0, 2*np.pi, 1000), 100.0, use_jacobian=use_jacobian)
|
|
89
|
+
np.testing.assert_allclose(filter.state.mean(), true_state, atol=1e-2)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Tests
|
|
93
|
+
|
|
94
|
+
The library includes a set of tests to ensure the functionality of the filtering and smoothing algorithms. These can be run as follows:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python test_filtering_smoothing.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
### `distributions.py`
|
|
103
|
+
|
|
104
|
+
Defines the Gaussian distribution class used for state representation and propagation.
|
|
105
|
+
|
|
106
|
+
### `filtering.py`
|
|
107
|
+
|
|
108
|
+
Implements the `BayesianFilter` class, responsible for running the filtering process with predict and update steps.
|
|
109
|
+
|
|
110
|
+
### `model.py`
|
|
111
|
+
|
|
112
|
+
Contains the `StateTransitionModel` class, representing the state transition model.
|
|
113
|
+
|
|
114
|
+
### `observation.py`
|
|
115
|
+
|
|
116
|
+
Defines the `Observation` class, representing the observation model.
|
|
117
|
+
|
|
118
|
+
### `smoothing.py`
|
|
119
|
+
|
|
120
|
+
Implements the `RTS` class for Rauch-Tung-Striebel smoothing.
|
|
121
|
+
|
|
122
|
+
### `unscented.py`
|
|
123
|
+
|
|
124
|
+
Provides functions for the unscented transform, including `unscented_transform` and `propagate_gaussian`.
|
|
125
|
+
|
|
126
|
+
### `utilities.py`
|
|
127
|
+
|
|
128
|
+
Contains utility functions like `propagate_covariance`.
|
|
129
|
+
|
|
130
|
+
### `test_filtering_smoothing.py`
|
|
131
|
+
|
|
132
|
+
Includes tests for filtering and smoothing to validate the implementation.
|
|
133
|
+
|
|
134
|
+
## Author
|
|
135
|
+
|
|
136
|
+
Hugo Hadfield
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
bayesfilter/__init__.py
|
|
4
|
+
bayesfilter/distributions.py
|
|
5
|
+
bayesfilter/filtering.py
|
|
6
|
+
bayesfilter/model.py
|
|
7
|
+
bayesfilter/observation.py
|
|
8
|
+
bayesfilter/smoothing.py
|
|
9
|
+
bayesfilter/test_filtering_smoothing.py
|
|
10
|
+
bayesfilter/unscented.py
|
|
11
|
+
bayesfilter/utilities.py
|
|
12
|
+
bayesfilter.egg-info/PKG-INFO
|
|
13
|
+
bayesfilter.egg-info/SOURCES.txt
|
|
14
|
+
bayesfilter.egg-info/dependency_links.txt
|
|
15
|
+
bayesfilter.egg-info/requires.txt
|
|
16
|
+
bayesfilter.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bayesfilter
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name='bayesfilter',
|
|
6
|
+
version='0.0.2',
|
|
7
|
+
packages=['bayesfilter'],
|
|
8
|
+
install_requires=[
|
|
9
|
+
'numpy',
|
|
10
|
+
],
|
|
11
|
+
license='MIT',
|
|
12
|
+
author='Hugo Hadfield',
|
|
13
|
+
long_description=open('README.md').read(),
|
|
14
|
+
long_description_content_type='text/markdown',
|
|
15
|
+
)
|