fast-allan-variance 1.0.0__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.
- fast_allan_variance-1.0.0/PKG-INFO +124 -0
- fast_allan_variance-1.0.0/README.md +102 -0
- fast_allan_variance-1.0.0/allan_variance/__init__.py +3 -0
- fast_allan_variance-1.0.0/allan_variance/analysis.py +367 -0
- fast_allan_variance-1.0.0/allan_variance/base.py +118 -0
- fast_allan_variance-1.0.0/allan_variance/imu_data.py +28 -0
- fast_allan_variance-1.0.0/allan_variance/ros.py +33 -0
- fast_allan_variance-1.0.0/allan_variance/ros2.py +33 -0
- fast_allan_variance-1.0.0/allan_variance/rosbag_reader.py +79 -0
- fast_allan_variance-1.0.0/allan_variance/statistics.py +84 -0
- fast_allan_variance-1.0.0/pyproject.toml +37 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fast_allan_variance
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Efficient Tools for Allan Variance Analysis
|
|
5
|
+
Keywords: Research,Inertial,Allan Variance
|
|
6
|
+
Author: Varun Agrawal
|
|
7
|
+
Author-email: Varun Agrawal <varagrawal@gmail.com>
|
|
8
|
+
License: BSD 3-Clause License
|
|
9
|
+
Requires-Dist: numpy>=2.2.4
|
|
10
|
+
Requires-Dist: pyaml>=21.10.1
|
|
11
|
+
Requires-Dist: attrdict>=2.0.1
|
|
12
|
+
Requires-Dist: tqdm>=4.62.2
|
|
13
|
+
Requires-Dist: loguru>=0.6.0
|
|
14
|
+
Requires-Dist: matplotlib>=3.10.7
|
|
15
|
+
Requires-Dist: pytest>=7.1.1 ; extra == 'dev'
|
|
16
|
+
Requires-Dist: pylint>=2.13.2 ; extra == 'dev'
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Project-URL: repository, https://github.com/varunagrawal/allan_variance
|
|
19
|
+
Project-URL: homepage, https://github.com/varunagrawal/allan_variance
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Fast Allan Variance
|
|
24
|
+
|
|
25
|
+
Tools for Allan Variance Analysis
|
|
26
|
+
|
|
27
|
+
This library provides tools for performing Allan Variance Analysis on IMU data in order to calibrate the IMU parameters.
|
|
28
|
+
This is done by computing the Angle Random Walk (ARW), Bias Instability and Gyro Random Walk for the gyroscope, and Velocity Random Walk (VRW), Bias Instability and Accel Random Walk for the accelerometer.
|
|
29
|
+
|
|
30
|
+
While plenty of libraries exist for this purpose, this library stands out for its immense efficiency, use of pure Python (no bindings or compilation needed), and ease of use, while being fully unit tested for ease-of-mind when adopting it for your use case.
|
|
31
|
+
|
|
32
|
+
## Faster
|
|
33
|
+
|
|
34
|
+
The popular [allan_variance_ros](https://github.com/ori-drs/allan_variance_ros) package (written in C++), when run on the example `imu_simulation.bag` takes:
|
|
35
|
+
|
|
36
|
+
`611.90s user 204.06s system 96% cpu 14:06.21 total`
|
|
37
|
+
|
|
38
|
+
Our library takes:
|
|
39
|
+
|
|
40
|
+
`40.64s user 2.04s system 91% cpu 46.558 total`
|
|
41
|
+
|
|
42
|
+
which is a 15x speed improvement!
|
|
43
|
+
|
|
44
|
+
## Easier
|
|
45
|
+
|
|
46
|
+
- There is no ROS dependency! But still useable within ROS.
|
|
47
|
+
- Since this library is written completely in Python, you can simple `import allan_variance` and use the tools.
|
|
48
|
+
- `pip` installable.
|
|
49
|
+
- Outputs an `imu.yaml` file following the [Kalibr](https://github.com/ethz-asl/kalibr) format.
|
|
50
|
+
|
|
51
|
+
## Collecting IMU Data
|
|
52
|
+
|
|
53
|
+
Please follow the below steps to collect the data needed to calibrate the IMU.
|
|
54
|
+
|
|
55
|
+
1. Place your IMU on some damped surface and record your IMU data to the file format of your choice. Please be sure to record **at least** 3 hours of data. The longer the sequence, the more accurate the results.
|
|
56
|
+
|
|
57
|
+
2. **Recommended** Reorganize messages by timestamp.
|
|
58
|
+
|
|
59
|
+
3. Define an interface for reading the data file and loading it as a `Tx6` `numpy` array where `T` is the number of data samples and we have 3-dimensional gyroscrope and 3-dimensional accelerometer data.
|
|
60
|
+
|
|
61
|
+
4. Create a configuration YAML file which specifies the IMU rate (Hz) and the measure rate (Hz) which is the rate at which data is measured/subsampled. You can add additional parameters for your particular interface. An example config file is availabe in [config/sim.yaml](config/sim.yaml).
|
|
62
|
+
|
|
63
|
+
5. Run Allan Variance computation tool. Please see [analyze_rosbag.py](scripts/analyze_rosbag.py) for an example script using ROS bag data.
|
|
64
|
+
|
|
65
|
+
6. The result is a generated `imu.yaml` file in the Kalibr format.
|
|
66
|
+
|
|
67
|
+
## Example Data
|
|
68
|
+
|
|
69
|
+
We use the 3 hour log of a [Realsense D435i IMU](https://drive.google.com/file/d/1ovI2NvYR52Axt-KuRs5HjVk7-57ky72H/view?usp=sharing) with timestamps already re-arranged, as provided by [raabuchanan](https://github.com/raabuchanan).
|
|
70
|
+
|
|
71
|
+
Our library automatically generates a Kalibr compatible file as `imu.yaml`:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
# Accelerometer
|
|
75
|
+
accelerometer_noise_density: 0.006308226052016165
|
|
76
|
+
accelerometer_random_walk: 0.00011673723527962174
|
|
77
|
+
|
|
78
|
+
# Gyroscope
|
|
79
|
+
gyroscope_noise_density: 0.00015198973532354657
|
|
80
|
+
gyroscope_random_walk: 2.664506559330434e-06
|
|
81
|
+
|
|
82
|
+
update_rate: 400.0 # Make sure this is correct
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Author
|
|
87
|
+
|
|
88
|
+
[Varun Agrawal](varunagrawal.github.io)
|
|
89
|
+
|
|
90
|
+
If you use this package for academic work, please consider using the citation below:
|
|
91
|
+
|
|
92
|
+
```bib
|
|
93
|
+
@software{FastAllanVariance,
|
|
94
|
+
author = {Varun Agrawal},
|
|
95
|
+
title = {Fast Allan Variance Analysis},
|
|
96
|
+
month = June,
|
|
97
|
+
year = 2026,
|
|
98
|
+
version = {1.0.0},
|
|
99
|
+
url = {https://github.com/varunagrawal/allan_variance}}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## References
|
|
104
|
+
|
|
105
|
+
- [Indirect Kalman Filter for 3D Attitude Estimation, Trawny & Roumeliotis](http://mars.cs.umn.edu/tr/reports/Trawny05b.pdf)
|
|
106
|
+
- [An introduction to inertial navigation, Oliver Woodman](https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-696.pdf)
|
|
107
|
+
- [Characterization of Errors and Noises in MEMS Inertial Sensors Using Allan Variance Method, Leslie Barreda Pupo](https://upcommons.upc.edu/bitstream/handle/2117/103849/MScLeslieB.pdf?sequence=1&isAllowed=y)
|
|
108
|
+
- [Kalibr IMU Noise Documentation](https://github.com/ethz-asl/kalibr/wiki/IMU-Noise-Model)
|
|
109
|
+
|
|
110
|
+
## Local Development
|
|
111
|
+
|
|
112
|
+
We use `uv` to manage the project.
|
|
113
|
+
|
|
114
|
+
To run unit tests:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
uv run pytest
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
To run an example script:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
uv run python scripts/analyze_rosbag.py config/sim.yaml imu_simulation.bag
|
|
124
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Fast Allan Variance
|
|
2
|
+
|
|
3
|
+
Tools for Allan Variance Analysis
|
|
4
|
+
|
|
5
|
+
This library provides tools for performing Allan Variance Analysis on IMU data in order to calibrate the IMU parameters.
|
|
6
|
+
This is done by computing the Angle Random Walk (ARW), Bias Instability and Gyro Random Walk for the gyroscope, and Velocity Random Walk (VRW), Bias Instability and Accel Random Walk for the accelerometer.
|
|
7
|
+
|
|
8
|
+
While plenty of libraries exist for this purpose, this library stands out for its immense efficiency, use of pure Python (no bindings or compilation needed), and ease of use, while being fully unit tested for ease-of-mind when adopting it for your use case.
|
|
9
|
+
|
|
10
|
+
## Faster
|
|
11
|
+
|
|
12
|
+
The popular [allan_variance_ros](https://github.com/ori-drs/allan_variance_ros) package (written in C++), when run on the example `imu_simulation.bag` takes:
|
|
13
|
+
|
|
14
|
+
`611.90s user 204.06s system 96% cpu 14:06.21 total`
|
|
15
|
+
|
|
16
|
+
Our library takes:
|
|
17
|
+
|
|
18
|
+
`40.64s user 2.04s system 91% cpu 46.558 total`
|
|
19
|
+
|
|
20
|
+
which is a 15x speed improvement!
|
|
21
|
+
|
|
22
|
+
## Easier
|
|
23
|
+
|
|
24
|
+
- There is no ROS dependency! But still useable within ROS.
|
|
25
|
+
- Since this library is written completely in Python, you can simple `import allan_variance` and use the tools.
|
|
26
|
+
- `pip` installable.
|
|
27
|
+
- Outputs an `imu.yaml` file following the [Kalibr](https://github.com/ethz-asl/kalibr) format.
|
|
28
|
+
|
|
29
|
+
## Collecting IMU Data
|
|
30
|
+
|
|
31
|
+
Please follow the below steps to collect the data needed to calibrate the IMU.
|
|
32
|
+
|
|
33
|
+
1. Place your IMU on some damped surface and record your IMU data to the file format of your choice. Please be sure to record **at least** 3 hours of data. The longer the sequence, the more accurate the results.
|
|
34
|
+
|
|
35
|
+
2. **Recommended** Reorganize messages by timestamp.
|
|
36
|
+
|
|
37
|
+
3. Define an interface for reading the data file and loading it as a `Tx6` `numpy` array where `T` is the number of data samples and we have 3-dimensional gyroscrope and 3-dimensional accelerometer data.
|
|
38
|
+
|
|
39
|
+
4. Create a configuration YAML file which specifies the IMU rate (Hz) and the measure rate (Hz) which is the rate at which data is measured/subsampled. You can add additional parameters for your particular interface. An example config file is availabe in [config/sim.yaml](config/sim.yaml).
|
|
40
|
+
|
|
41
|
+
5. Run Allan Variance computation tool. Please see [analyze_rosbag.py](scripts/analyze_rosbag.py) for an example script using ROS bag data.
|
|
42
|
+
|
|
43
|
+
6. The result is a generated `imu.yaml` file in the Kalibr format.
|
|
44
|
+
|
|
45
|
+
## Example Data
|
|
46
|
+
|
|
47
|
+
We use the 3 hour log of a [Realsense D435i IMU](https://drive.google.com/file/d/1ovI2NvYR52Axt-KuRs5HjVk7-57ky72H/view?usp=sharing) with timestamps already re-arranged, as provided by [raabuchanan](https://github.com/raabuchanan).
|
|
48
|
+
|
|
49
|
+
Our library automatically generates a Kalibr compatible file as `imu.yaml`:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
# Accelerometer
|
|
53
|
+
accelerometer_noise_density: 0.006308226052016165
|
|
54
|
+
accelerometer_random_walk: 0.00011673723527962174
|
|
55
|
+
|
|
56
|
+
# Gyroscope
|
|
57
|
+
gyroscope_noise_density: 0.00015198973532354657
|
|
58
|
+
gyroscope_random_walk: 2.664506559330434e-06
|
|
59
|
+
|
|
60
|
+
update_rate: 400.0 # Make sure this is correct
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Author
|
|
65
|
+
|
|
66
|
+
[Varun Agrawal](varunagrawal.github.io)
|
|
67
|
+
|
|
68
|
+
If you use this package for academic work, please consider using the citation below:
|
|
69
|
+
|
|
70
|
+
```bib
|
|
71
|
+
@software{FastAllanVariance,
|
|
72
|
+
author = {Varun Agrawal},
|
|
73
|
+
title = {Fast Allan Variance Analysis},
|
|
74
|
+
month = June,
|
|
75
|
+
year = 2026,
|
|
76
|
+
version = {1.0.0},
|
|
77
|
+
url = {https://github.com/varunagrawal/allan_variance}}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## References
|
|
82
|
+
|
|
83
|
+
- [Indirect Kalman Filter for 3D Attitude Estimation, Trawny & Roumeliotis](http://mars.cs.umn.edu/tr/reports/Trawny05b.pdf)
|
|
84
|
+
- [An introduction to inertial navigation, Oliver Woodman](https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-696.pdf)
|
|
85
|
+
- [Characterization of Errors and Noises in MEMS Inertial Sensors Using Allan Variance Method, Leslie Barreda Pupo](https://upcommons.upc.edu/bitstream/handle/2117/103849/MScLeslieB.pdf?sequence=1&isAllowed=y)
|
|
86
|
+
- [Kalibr IMU Noise Documentation](https://github.com/ethz-asl/kalibr/wiki/IMU-Noise-Model)
|
|
87
|
+
|
|
88
|
+
## Local Development
|
|
89
|
+
|
|
90
|
+
We use `uv` to manage the project.
|
|
91
|
+
|
|
92
|
+
To run unit tests:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
uv run pytest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To run an example script:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv run python scripts/analyze_rosbag.py config/sim.yaml imu_simulation.bag
|
|
102
|
+
```
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions to perform analysis of the Allan Deviations to get the IMU parameters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Callable, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from matplotlib import pyplot as plt
|
|
9
|
+
from scipy.optimize import curve_fit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def line_func(x, m, b):
|
|
13
|
+
"""The line function y = mx + b"""
|
|
14
|
+
return m * x + b
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_intercept(x, y, m, b):
|
|
18
|
+
"""Get the x and y intercepts of the line"""
|
|
19
|
+
logx = np.log(x)
|
|
20
|
+
logy = np.log(y)
|
|
21
|
+
# pylint: disable=unbalanced-tuple-unpacking
|
|
22
|
+
coeffs, _ = curve_fit(
|
|
23
|
+
line_func, logx, logy, bounds=([m, -np.inf], [m + 0.001, np.inf])
|
|
24
|
+
)
|
|
25
|
+
poly = np.poly1d(coeffs)
|
|
26
|
+
|
|
27
|
+
def yfit(x):
|
|
28
|
+
return np.exp(poly(np.log(x)))
|
|
29
|
+
|
|
30
|
+
return yfit(b), yfit
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_prediction(
|
|
34
|
+
tau, q_quantization=0, q_white=0, q_bias_instability=0, q_walk=0, q_ramp=0
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Given fitted IMU noise model, generate measurement prediction at `tau`.
|
|
38
|
+
"""
|
|
39
|
+
n = len(tau)
|
|
40
|
+
|
|
41
|
+
A = np.empty((n, 5))
|
|
42
|
+
A[:, 0] = 3 / tau**2
|
|
43
|
+
A[:, 1] = 1 / tau
|
|
44
|
+
A[:, 2] = 2 * np.log(2) / np.pi
|
|
45
|
+
A[:, 3] = tau / 3
|
|
46
|
+
A[:, 4] = tau**2 / 2
|
|
47
|
+
|
|
48
|
+
params = np.array(
|
|
49
|
+
[q_quantization**2, q_white**2, q_bias_instability**2, q_walk**2, q_ramp**2]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return np.sqrt(A.dot(params))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def compute_white_noise_params(period, measurements, white_noise_break_point):
|
|
56
|
+
"""
|
|
57
|
+
Compute the ARW/VRW generated from the white noise component.
|
|
58
|
+
Found by fitting a line with gradient=-0.5 at intercept t=1
|
|
59
|
+
"""
|
|
60
|
+
wn_intercept = np.zeros(3)
|
|
61
|
+
wn_fit_fn = [None] * 3
|
|
62
|
+
for idx, _ in enumerate("xyz"):
|
|
63
|
+
wn_intercept[idx], wn_fit_fn[idx] = get_intercept(
|
|
64
|
+
period[0:white_noise_break_point],
|
|
65
|
+
measurements[0:white_noise_break_point, idx],
|
|
66
|
+
-0.5,
|
|
67
|
+
1.0,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return wn_intercept, wn_fit_fn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compute_rate_random_walk(period, measurements):
|
|
74
|
+
"""
|
|
75
|
+
Compute the Rate Random Walk for the given measurements.
|
|
76
|
+
This corresponds to the y-intercept of the line with gradient=0.5 and x-intercept=3.0.
|
|
77
|
+
"""
|
|
78
|
+
rr_intercept = np.zeros(3)
|
|
79
|
+
rr_fit_fn = [None] * 3
|
|
80
|
+
for idx, _ in enumerate("xyz"):
|
|
81
|
+
axis_measurements = measurements[:, idx]
|
|
82
|
+
rr_intercept[idx], rr_fit_fn[idx] = get_intercept(
|
|
83
|
+
period, axis_measurements, 0.5, 3.0
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return rr_intercept, rr_fit_fn
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compute_bias_instability(measurement: np.ndarray):
|
|
90
|
+
"""
|
|
91
|
+
Compute the bias instability values.
|
|
92
|
+
These are the bias drift standard deviations.
|
|
93
|
+
"""
|
|
94
|
+
measurement_min = np.amin(measurement, axis=0)
|
|
95
|
+
measurement_argmin = np.argmin(measurement, axis=0)
|
|
96
|
+
return measurement_min, measurement_argmin
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def plot_loglog(
|
|
100
|
+
period: np.ndarray,
|
|
101
|
+
measurements: np.ndarray,
|
|
102
|
+
fit_wn: List[Callable],
|
|
103
|
+
fit_rr: List[Callable],
|
|
104
|
+
wn_intercept: List[float],
|
|
105
|
+
rr_intercept: List[float],
|
|
106
|
+
measurement_min: List[float],
|
|
107
|
+
measurement_min_index: List[int],
|
|
108
|
+
average_white_noise: float,
|
|
109
|
+
average_bias_instability: float,
|
|
110
|
+
average_random_walk: float,
|
|
111
|
+
measurement_type: str,
|
|
112
|
+
sensor_type: str,
|
|
113
|
+
units: str,
|
|
114
|
+
dpi=90,
|
|
115
|
+
figsize=(16, 9),
|
|
116
|
+
):
|
|
117
|
+
"""Plot Allan Deviations on LogLog scale.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
period (np.ndarray): _description_
|
|
121
|
+
measurements (np.ndarray): _description_
|
|
122
|
+
fit_wn (List[Callable]): _description_
|
|
123
|
+
fit_rr (List[Callable]): _description_
|
|
124
|
+
wn_intercept (List[float]): _description_
|
|
125
|
+
rr_intercept (List[float]): _description_
|
|
126
|
+
measurement_min (List[float]): _description_
|
|
127
|
+
measurement_min_index (List[int]): _description_
|
|
128
|
+
average_white_noise (float): _description_
|
|
129
|
+
average_bias_instability (float): _description_
|
|
130
|
+
average_random_walk (float): _description_
|
|
131
|
+
measurement_type (str): _description_
|
|
132
|
+
sensor_type (str): _description_
|
|
133
|
+
units (str): _description_
|
|
134
|
+
dpi (int, optional): _description_. Defaults to 90.
|
|
135
|
+
figsize (tuple, optional): _description_. Defaults to (16, 9).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
fig = plt.figure(num=measurement_type, dpi=dpi, figsize=figsize)
|
|
139
|
+
|
|
140
|
+
plt.loglog(period, measurements[:, 0], "r--", label="X")
|
|
141
|
+
plt.loglog(period, measurements[:, 1], "g--", label="Y")
|
|
142
|
+
plt.loglog(period, measurements[:, 2], "b--", label="Z")
|
|
143
|
+
|
|
144
|
+
for idx, c in enumerate("rgb"):
|
|
145
|
+
if idx == 2:
|
|
146
|
+
wn_label = "White noise fit line"
|
|
147
|
+
rr_label = "Random Rate fit line"
|
|
148
|
+
else:
|
|
149
|
+
wn_label = ""
|
|
150
|
+
rr_label = ""
|
|
151
|
+
|
|
152
|
+
plt.loglog(period, fit_wn[idx](period), "m-", label=wn_label)
|
|
153
|
+
plt.loglog(period, fit_rr[idx](period), "y-", label=rr_label)
|
|
154
|
+
|
|
155
|
+
plt.loglog(1.0, wn_intercept[idx], f"{c}o", markersize=20)
|
|
156
|
+
plt.loglog(3.0, rr_intercept[idx], f"{c}*", markersize=20)
|
|
157
|
+
|
|
158
|
+
plt.loglog(
|
|
159
|
+
period[measurement_min_index[idx]],
|
|
160
|
+
measurement_min[idx],
|
|
161
|
+
f"{c}^",
|
|
162
|
+
markersize=20,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
fitted_model = generate_prediction(
|
|
166
|
+
period,
|
|
167
|
+
q_white=average_white_noise,
|
|
168
|
+
q_bias_instability=average_bias_instability,
|
|
169
|
+
q_walk=average_random_walk,
|
|
170
|
+
)
|
|
171
|
+
plt.loglog(period, fitted_model, "-k", label="fitted model")
|
|
172
|
+
|
|
173
|
+
plt.title(sensor_type, fontsize=30)
|
|
174
|
+
plt.ylabel(f"Allan Deviation {units}", fontsize=30)
|
|
175
|
+
plt.legend(fontsize=25)
|
|
176
|
+
plt.grid(True)
|
|
177
|
+
plt.xlabel("Period (s)", fontsize=30)
|
|
178
|
+
plt.tight_layout()
|
|
179
|
+
|
|
180
|
+
plt.draw()
|
|
181
|
+
plt.close()
|
|
182
|
+
|
|
183
|
+
fig.savefig(f"{measurement_type.lower()}.png", dpi=600, bbox_inches="tight")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def accelerometer_analysis(
|
|
187
|
+
period, acceleration, white_noise_break_point, show_plots: bool = True
|
|
188
|
+
):
|
|
189
|
+
"""Analyze the accelerometer measurements to get accelerometer parameters."""
|
|
190
|
+
# Compute VRW from the white noise
|
|
191
|
+
# gradient=-0.5, intercept at t=1
|
|
192
|
+
accel_wn_intercept, accel_fit_wn = compute_white_noise_params(
|
|
193
|
+
period, acceleration, white_noise_break_point
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Compute rate random walk
|
|
197
|
+
# gradient=0.5, intercept at t=3
|
|
198
|
+
accel_rr_intercept, accel_fit_rr = compute_rate_random_walk(period, acceleration)
|
|
199
|
+
|
|
200
|
+
accel_min, accel_min_index = compute_bias_instability(acceleration)
|
|
201
|
+
|
|
202
|
+
print("ACCELEROMETER:")
|
|
203
|
+
for idx, axis in enumerate("XYZ"):
|
|
204
|
+
print(
|
|
205
|
+
f"{axis} Velocity Random Walk: {accel_wn_intercept[idx]: .5f} m/s/sqrt(s)",
|
|
206
|
+
f"{accel_wn_intercept[idx] * 60: .5f} m/s/sqrt(hr)",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
for idx, axis in enumerate("XYZ"):
|
|
210
|
+
print(
|
|
211
|
+
f"{axis} Bias Instability: {accel_min[idx]: .5f} m/s^2",
|
|
212
|
+
f"{accel_min[idx] * 3600 * 3600: .5f} m/hr^2",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
for idx, axis in enumerate("XYZ"):
|
|
216
|
+
print(f"{axis} Accel Random Walk: {accel_rr_intercept[idx]: .5f} m/s^2/sqrt(s)")
|
|
217
|
+
|
|
218
|
+
average_acc_white_noise = accel_wn_intercept.mean()
|
|
219
|
+
average_acc_bias_instability = accel_min.mean()
|
|
220
|
+
average_acc_random_walk = accel_rr_intercept.mean()
|
|
221
|
+
|
|
222
|
+
# Use worst value
|
|
223
|
+
worst_accel_white_noise = np.amax(accel_wn_intercept)
|
|
224
|
+
worst_accel_random_walk = np.amax(accel_rr_intercept)
|
|
225
|
+
|
|
226
|
+
if show_plots:
|
|
227
|
+
plot_loglog(
|
|
228
|
+
period,
|
|
229
|
+
acceleration,
|
|
230
|
+
accel_fit_wn,
|
|
231
|
+
accel_fit_rr,
|
|
232
|
+
accel_wn_intercept,
|
|
233
|
+
accel_rr_intercept,
|
|
234
|
+
accel_min,
|
|
235
|
+
accel_min_index,
|
|
236
|
+
average_acc_white_noise,
|
|
237
|
+
average_acc_bias_instability,
|
|
238
|
+
average_acc_random_walk,
|
|
239
|
+
"Acceleration",
|
|
240
|
+
"Accelerometer",
|
|
241
|
+
"m/s^2",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return worst_accel_white_noise, worst_accel_random_walk
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def gyroscope_analysis(
|
|
248
|
+
period, rotation_rate, white_noise_break_point, show_plots: bool = True
|
|
249
|
+
):
|
|
250
|
+
"""Analyze the gyroscope measurements to get gyroscope parameters."""
|
|
251
|
+
# Compute ARW from the white noise
|
|
252
|
+
# gradient=-0.5, intercept at t=1
|
|
253
|
+
gyro_wn_intercept, gyro_fit_wn = compute_white_noise_params(
|
|
254
|
+
period, rotation_rate, white_noise_break_point
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Compute rate random walk
|
|
258
|
+
# gradient=0.5, intercept at t=3
|
|
259
|
+
gyro_rr_intercept, gyro_fit_rr = compute_rate_random_walk(period, rotation_rate)
|
|
260
|
+
|
|
261
|
+
gyro_min, gyro_min_index = compute_bias_instability(rotation_rate)
|
|
262
|
+
|
|
263
|
+
print("GYROSCOPE:")
|
|
264
|
+
for idx, axis in enumerate("XYZ"):
|
|
265
|
+
print(
|
|
266
|
+
f"{axis} Angle Random Walk: {gyro_wn_intercept[idx]: .5f} deg/sqrt(s)",
|
|
267
|
+
f"{gyro_wn_intercept[idx] * 60: .5f} deg/sqrt(hr)",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
for idx, axis in enumerate("XYZ"):
|
|
271
|
+
print(
|
|
272
|
+
f"{axis} Bias Instability: {gyro_min[idx]: .5f} deg/s {gyro_min[idx] * 60 * 60: .5f} deg/hr"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
for idx, axis in enumerate("XYZ"):
|
|
276
|
+
print(f"{axis} Rate Random Walk: {gyro_rr_intercept[idx]: .5f} deg/s/sqrt(s)")
|
|
277
|
+
|
|
278
|
+
average_gyro_white_noise = gyro_wn_intercept.mean()
|
|
279
|
+
average_gyro_bias_instability = gyro_min.mean()
|
|
280
|
+
average_gyro_random_walk = gyro_rr_intercept.mean()
|
|
281
|
+
|
|
282
|
+
# use worst value
|
|
283
|
+
worst_gyro_white_noise = np.amax(gyro_wn_intercept)
|
|
284
|
+
worst_gyro_random_walk = np.amax(gyro_rr_intercept)
|
|
285
|
+
|
|
286
|
+
if show_plots:
|
|
287
|
+
plot_loglog(
|
|
288
|
+
period,
|
|
289
|
+
rotation_rate,
|
|
290
|
+
gyro_fit_wn,
|
|
291
|
+
gyro_fit_rr,
|
|
292
|
+
gyro_wn_intercept,
|
|
293
|
+
gyro_rr_intercept,
|
|
294
|
+
gyro_min,
|
|
295
|
+
gyro_min_index,
|
|
296
|
+
average_gyro_white_noise,
|
|
297
|
+
average_gyro_bias_instability,
|
|
298
|
+
average_gyro_random_walk,
|
|
299
|
+
measurement_type="Gyro",
|
|
300
|
+
sensor_type="Gyroscope",
|
|
301
|
+
units="deg/s",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return worst_gyro_white_noise, worst_gyro_random_walk
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def write_imu_yaml(
|
|
308
|
+
worst_accel_white_noise: float,
|
|
309
|
+
worst_accel_random_walk: float,
|
|
310
|
+
worst_gyro_white_noise: float,
|
|
311
|
+
worst_gyro_random_walk: float,
|
|
312
|
+
update_rate: int,
|
|
313
|
+
):
|
|
314
|
+
"""
|
|
315
|
+
Write IMU calibration parameters to YAML file.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
worst_accel_white_noise (float): Accelerometer white noise
|
|
319
|
+
worst_accel_random_walk (float): Accelerometer bias random walk
|
|
320
|
+
worst_gyro_white_noise (float): Gyroscope white noise
|
|
321
|
+
worst_gyro_random_walk (float): Gyroscope bias random walk
|
|
322
|
+
update_rate (int): The IMU update rate.
|
|
323
|
+
"""
|
|
324
|
+
print("Writing Kalibr imu.yaml file.")
|
|
325
|
+
with open("imu.yaml", "w") as yaml_file:
|
|
326
|
+
yaml_file.write("# Accelerometer\n")
|
|
327
|
+
yaml_file.write(f"accelerometer_noise_density: {worst_accel_white_noise}\n")
|
|
328
|
+
yaml_file.write(f"accelerometer_random_walk: {worst_accel_random_walk}\n")
|
|
329
|
+
yaml_file.write("\n")
|
|
330
|
+
|
|
331
|
+
yaml_file.write("# Gyroscope\n")
|
|
332
|
+
# Convert back to radians here
|
|
333
|
+
yaml_file.write(
|
|
334
|
+
f"gyroscope_noise_density: {worst_gyro_white_noise * np.pi / 180}\n"
|
|
335
|
+
)
|
|
336
|
+
yaml_file.write(
|
|
337
|
+
f"gyroscope_random_walk: {worst_gyro_random_walk * np.pi / 180}\n"
|
|
338
|
+
)
|
|
339
|
+
yaml_file.write("\n")
|
|
340
|
+
|
|
341
|
+
yaml_file.write(f"update_rate: {update_rate} # Make sure this is correct\n")
|
|
342
|
+
|
|
343
|
+
print("Make sure to update rostopic and rate.")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def analyze(period, allan_deviations, update_rate: int, show_plots: bool = True):
|
|
347
|
+
"""Analyze the Allan Deviations to get IMU parameters"""
|
|
348
|
+
acceleration = allan_deviations[:, 0:3]
|
|
349
|
+
rotation_rate = allan_deviations[:, 3:6]
|
|
350
|
+
|
|
351
|
+
white_noise_break_point = np.where(period == 10)[0][0]
|
|
352
|
+
|
|
353
|
+
worst_accel_white_noise, worst_accel_random_walk = accelerometer_analysis(
|
|
354
|
+
period, acceleration, white_noise_break_point, show_plots=show_plots
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
worst_gyro_white_noise, worst_gyro_random_walk = gyroscope_analysis(
|
|
358
|
+
period, rotation_rate, white_noise_break_point, show_plots=show_plots
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
write_imu_yaml(
|
|
362
|
+
worst_accel_white_noise,
|
|
363
|
+
worst_accel_random_walk,
|
|
364
|
+
worst_gyro_white_noise,
|
|
365
|
+
worst_gyro_random_walk,
|
|
366
|
+
update_rate=update_rate,
|
|
367
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Base module to Allan Variance Analysis"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import yaml
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from allan_variance.analysis import analyze
|
|
11
|
+
from allan_variance.statistics import compute_allan_variances
|
|
12
|
+
|
|
13
|
+
FilePath = Union[str, Path]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Config:
|
|
17
|
+
"""Class for storing IMU configuration info."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config_file: FilePath):
|
|
20
|
+
with open(config_file, "r") as stream:
|
|
21
|
+
self.config_ = yaml.safe_load(stream)
|
|
22
|
+
|
|
23
|
+
self.imu_topic_ = self.config_["imu_topic"]
|
|
24
|
+
self.imu_rate_ = self.config_["imu_rate"]
|
|
25
|
+
self.measure_rate_ = self.config_["measure_rate"]
|
|
26
|
+
self.sequence_time_ = self.config_["sequence_time"]
|
|
27
|
+
|
|
28
|
+
self.imu_skip_ = self.imu_rate_ // self.measure_rate_
|
|
29
|
+
|
|
30
|
+
def config(self, key: str = ""):
|
|
31
|
+
"""Getter for the config."""
|
|
32
|
+
if key:
|
|
33
|
+
return self.config_[key]
|
|
34
|
+
else:
|
|
35
|
+
return self.config_
|
|
36
|
+
|
|
37
|
+
def imu_topic(self):
|
|
38
|
+
"""Get the IMU topic."""
|
|
39
|
+
return self.imu_topic_
|
|
40
|
+
|
|
41
|
+
def imu_rate(self):
|
|
42
|
+
"""Get the IMU rate."""
|
|
43
|
+
return self.imu_rate_
|
|
44
|
+
|
|
45
|
+
def measure_rate(self):
|
|
46
|
+
"""Get the IMU measurement rate."""
|
|
47
|
+
return self.measure_rate_
|
|
48
|
+
|
|
49
|
+
def sequence_time(self):
|
|
50
|
+
"""Get the total sequence time."""
|
|
51
|
+
return self.sequence_time_
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AllanVariance(Config):
|
|
55
|
+
"""Main class to perform Allan Variance Analysis"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
config_file: FilePath,
|
|
60
|
+
output_path: FilePath,
|
|
61
|
+
overlap: int = 0,
|
|
62
|
+
period_min: float = 0.1,
|
|
63
|
+
period_max: float = 1000,
|
|
64
|
+
write_allan_deviations=False,
|
|
65
|
+
):
|
|
66
|
+
|
|
67
|
+
super().__init__(config_file=config_file)
|
|
68
|
+
|
|
69
|
+
self.allan_variance_file_ = Path(output_path) / "allan_variance.csv"
|
|
70
|
+
|
|
71
|
+
self.overlap_ = overlap
|
|
72
|
+
|
|
73
|
+
# Range we will sample from (e.g. 0.1s to 1000s)
|
|
74
|
+
self.period_min, self.period_max = period_min, period_max
|
|
75
|
+
|
|
76
|
+
self.write_allan_deviations_ = write_allan_deviations
|
|
77
|
+
|
|
78
|
+
def overlap(self):
|
|
79
|
+
"""Get the overlap."""
|
|
80
|
+
return self.overlap_
|
|
81
|
+
|
|
82
|
+
def __call__(self, data):
|
|
83
|
+
"""Run Allan Variance"""
|
|
84
|
+
return self.run(data)
|
|
85
|
+
|
|
86
|
+
def write_deviations(self, periods: np.ndarray, allan_deviations: np.ndarray):
|
|
87
|
+
"""Helper method to write the Allan Deviations to file."""
|
|
88
|
+
logger.info(f"Writing Allan Deviations to {self.allan_variance_file_}")
|
|
89
|
+
with open(self.allan_variance_file_, "w+") as av_writer:
|
|
90
|
+
for period, allan_deviation in zip(periods, allan_deviations):
|
|
91
|
+
# Convert to string for writing to file
|
|
92
|
+
allan_deviation_str = " ".join(map(str, allan_deviation.tolist()))
|
|
93
|
+
av_writer.write(f"{period} {allan_deviation_str}\n")
|
|
94
|
+
|
|
95
|
+
def run(self, data: np.ndarray):
|
|
96
|
+
"""Run Allan Variance Analysis
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
data (np.ndarray): A Tx6 data array where the first 3
|
|
100
|
+
columns are linear acceleration and the next 3
|
|
101
|
+
are angular velocity.
|
|
102
|
+
"""
|
|
103
|
+
# Assuming gyro data is in radians, convert to degrees
|
|
104
|
+
data[:, 3:6] = np.rad2deg(data[:, 3:6])
|
|
105
|
+
|
|
106
|
+
periods = np.arange(self.period_min, self.period_max, step=0.1)
|
|
107
|
+
|
|
108
|
+
logger.info("Computing Allan Variances")
|
|
109
|
+
allan_variances = compute_allan_variances(
|
|
110
|
+
data, periods, self.measure_rate_, self.overlap_
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
allan_deviations = np.sqrt(allan_variances)
|
|
114
|
+
|
|
115
|
+
if self.write_allan_deviations_:
|
|
116
|
+
self.write_deviations(periods, allan_deviations)
|
|
117
|
+
|
|
118
|
+
analyze(periods, allan_deviations, self.imu_rate_)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Helpers for IMU measurements and data"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImuMeasurement:
|
|
7
|
+
"""Class representing an IMU measurement."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
timestamp: float,
|
|
12
|
+
linear_acceleration: np.ndarray,
|
|
13
|
+
angular_velocity: np.ndarray,
|
|
14
|
+
):
|
|
15
|
+
"""Constructor
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
timestamp (float): Timestamp in nanoseconds.
|
|
19
|
+
linear_acceleration (np.ndarray): The linear acceleration measurement.
|
|
20
|
+
angular_velocity (np.ndarray): The angular velocity meausrement.
|
|
21
|
+
"""
|
|
22
|
+
self.ts = timestamp
|
|
23
|
+
self.a = linear_acceleration
|
|
24
|
+
self.w = angular_velocity
|
|
25
|
+
|
|
26
|
+
def asarray(self):
|
|
27
|
+
"""Return the measurement as a single array"""
|
|
28
|
+
return np.concatenate(([self.ts], self.a, self.w))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Module for performing Allan Variance Analysis with ROS1 bags."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from allan_variance.base import AllanVariance
|
|
7
|
+
|
|
8
|
+
FilePath = Union[str, Path]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AllanVarianceROS(AllanVariance):
|
|
12
|
+
"""Main class to perform Allan Variance Analysis"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config_file: FilePath, output_path: FilePath) -> None:
|
|
15
|
+
super().__init__(config_file=config_file, output_path=output_path)
|
|
16
|
+
|
|
17
|
+
def load_imu_buffer(self, data):
|
|
18
|
+
"""Load IMU buffer from the ROS bag."""
|
|
19
|
+
imu_counter = 0
|
|
20
|
+
|
|
21
|
+
for tNanoSecs in data:
|
|
22
|
+
imu_counter += 1
|
|
23
|
+
|
|
24
|
+
# Subsample IMU measurements
|
|
25
|
+
if (imu_counter % self.imu_skip_ != 0) or (
|
|
26
|
+
imu_counter / self.imu_rate_ > self.sequence_time_
|
|
27
|
+
):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if self.first_msg_:
|
|
31
|
+
self.first_msg_ = False
|
|
32
|
+
self.first_time_ = tNanoSecs
|
|
33
|
+
self.last_imu_time_ = tNanoSecs
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from allan_variance.base import AllanVariance
|
|
7
|
+
|
|
8
|
+
FilePath = Union[str, Path]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AllanVarianceROS2(AllanVariance):
|
|
12
|
+
"""Main class to perform Allan Variance Analysis"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config_file: FilePath, output_path: FilePath) -> None:
|
|
15
|
+
super().__init__(config_file=config_file, output_path=output_path)
|
|
16
|
+
|
|
17
|
+
def load_imu_buffer(self, data):
|
|
18
|
+
"""Load IMU buffer from the ROS bag."""
|
|
19
|
+
imu_counter = 0
|
|
20
|
+
|
|
21
|
+
for tNanoSecs in data:
|
|
22
|
+
imu_counter += 1
|
|
23
|
+
|
|
24
|
+
# Subsample IMU measurements
|
|
25
|
+
if (imu_counter % self.imu_skip_ != 0) or (
|
|
26
|
+
imu_counter / self.imu_rate_ > self.sequence_time_
|
|
27
|
+
):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if self.first_msg_:
|
|
31
|
+
self.first_msg_ = False
|
|
32
|
+
self.first_time_ = tNanoSecs
|
|
33
|
+
self.last_imu_time_ = tNanoSecs
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Module for reading ROS bags"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from rosbags.highlevel import AnyReader
|
|
8
|
+
from rosbags.typesys import Stores, get_typestore
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from allan_variance import FilePath, ImuMeasurement
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ROSBagReader:
|
|
15
|
+
"""Class to read ROS bag data without the need to install ROS."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
rosbag_path: FilePath,
|
|
20
|
+
topic: str,
|
|
21
|
+
imu_rate: float,
|
|
22
|
+
sequence_time: float,
|
|
23
|
+
imu_skip: float,
|
|
24
|
+
typestore=Stores.ROS1_NOETIC,
|
|
25
|
+
):
|
|
26
|
+
self.rosbag_path = Path(rosbag_path)
|
|
27
|
+
self.topic = topic # "/sensors/imu"
|
|
28
|
+
self.imu_rate_ = imu_rate
|
|
29
|
+
self.sequence_time_ = sequence_time
|
|
30
|
+
self.imu_skip_ = imu_skip
|
|
31
|
+
|
|
32
|
+
# Create a type store to use if the bag has no message definitions.
|
|
33
|
+
self.typestore = get_typestore(typestore)
|
|
34
|
+
|
|
35
|
+
def read(self):
|
|
36
|
+
"""Read the ROS bag and get the measurements."""
|
|
37
|
+
imu_buffer = []
|
|
38
|
+
|
|
39
|
+
logger.info(f"Loading bag from path: {self.rosbag_path}")
|
|
40
|
+
|
|
41
|
+
# Create reader instance and open for reading.
|
|
42
|
+
with AnyReader([self.rosbag_path], default_typestore=self.typestore) as reader:
|
|
43
|
+
messages = reader.messages(connections=reader.connections)
|
|
44
|
+
|
|
45
|
+
for counter, message in tqdm(
|
|
46
|
+
enumerate(messages), total=reader.message_count
|
|
47
|
+
):
|
|
48
|
+
connection, timestamp, rawdata = message
|
|
49
|
+
|
|
50
|
+
if connection.topic == self.topic:
|
|
51
|
+
# Subsample IMU measurements
|
|
52
|
+
if ((counter + 1) % self.imu_skip_ != 0) or (
|
|
53
|
+
counter / self.imu_rate_ > self.sequence_time_
|
|
54
|
+
):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
msg = reader.deserialize(rawdata, connection.msgtype)
|
|
58
|
+
|
|
59
|
+
timestamp = msg.header.stamp
|
|
60
|
+
ts_ns = timestamp.sec * 1000000000 + timestamp.nanosec
|
|
61
|
+
|
|
62
|
+
w = np.asarray(
|
|
63
|
+
[
|
|
64
|
+
msg.angular_velocity.x,
|
|
65
|
+
msg.angular_velocity.y,
|
|
66
|
+
msg.angular_velocity.z,
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
a = np.asarray(
|
|
70
|
+
[
|
|
71
|
+
msg.linear_acceleration.x,
|
|
72
|
+
msg.linear_acceleration.y,
|
|
73
|
+
msg.linear_acceleration.z,
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
imu_buffer.append(ImuMeasurement(ts_ns, a, w).asarray())
|
|
77
|
+
|
|
78
|
+
logger.info("Loaded all the data")
|
|
79
|
+
return np.asarray(imu_buffer)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Various functions for computing statistics on data."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _compute_cumsum(data):
|
|
8
|
+
"""
|
|
9
|
+
Compute cumulative sum with prepended zero row.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
data : (N, D)
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
cumsum : (N+1, D)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
D = data.shape[1]
|
|
21
|
+
|
|
22
|
+
return np.concatenate(
|
|
23
|
+
[
|
|
24
|
+
np.zeros((1, D), dtype=data.dtype),
|
|
25
|
+
np.cumsum(data, axis=0),
|
|
26
|
+
],
|
|
27
|
+
axis=0,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_bin_averages(cumsum: np.ndarray, bin_size: int, overlap: int):
|
|
32
|
+
"""
|
|
33
|
+
Compute the averages over bins of size `bin_size`,
|
|
34
|
+
with `overlap` amount of overlap.
|
|
35
|
+
"""
|
|
36
|
+
N = cumsum.shape[0] - 1
|
|
37
|
+
|
|
38
|
+
# Compute the stride for the sliding window based on the overlap
|
|
39
|
+
stride = max(1, round(bin_size * (1.0 - overlap)))
|
|
40
|
+
|
|
41
|
+
# Compute the starting indices of each bin
|
|
42
|
+
starts = np.arange(0, N - bin_size + 1, stride)
|
|
43
|
+
|
|
44
|
+
averages = (cumsum[starts + bin_size] - cumsum[starts]) / bin_size
|
|
45
|
+
|
|
46
|
+
return averages
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_allan_variances(data, periods, measure_rate=10, overlap=0.5):
|
|
50
|
+
"""Compute the Allan Variance given the averages map.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
periods (np.ndarray): The time periods between period_min and period_max with step 0.1.
|
|
54
|
+
period_max (float): The maximum period time.
|
|
55
|
+
measure_rate (float): The measurement rate of the IMU.
|
|
56
|
+
overlap (float): The overlap between bins.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List[np.ndarray]: Allan Variances for various time periods
|
|
60
|
+
from `period_min` to `period_max`.
|
|
61
|
+
"""
|
|
62
|
+
# Pre-allocate the Allan Variances
|
|
63
|
+
allan_variances = np.empty(periods.shape + (6,))
|
|
64
|
+
|
|
65
|
+
# Precompute the cumulative sum for efficient bin average computation
|
|
66
|
+
data_cumsum = _compute_cumsum(data)
|
|
67
|
+
|
|
68
|
+
for idx, period_time in tqdm(enumerate(periods), total=len(periods)):
|
|
69
|
+
max_bin_size = int(period_time * measure_rate)
|
|
70
|
+
bin_overlap = int(np.floor(max_bin_size * overlap))
|
|
71
|
+
|
|
72
|
+
# Compute the bin averages in the same loop
|
|
73
|
+
# This saves memory and is faster
|
|
74
|
+
averages = compute_bin_averages(data_cumsum, max_bin_size, bin_overlap)
|
|
75
|
+
|
|
76
|
+
n = len(averages)
|
|
77
|
+
|
|
78
|
+
d = np.sum(np.power(averages[1:] - averages[:-1], 2), axis=0)
|
|
79
|
+
|
|
80
|
+
allan_variance = d / (2 * (n - 1))
|
|
81
|
+
|
|
82
|
+
allan_variances[idx] = allan_variance
|
|
83
|
+
|
|
84
|
+
return allan_variances
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fast_allan_variance"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Efficient Tools for Allan Variance Analysis"
|
|
5
|
+
authors = [{ name = "Varun Agrawal", email = "varagrawal@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "BSD 3-Clause License" }
|
|
8
|
+
keywords = ['Research', 'Inertial', 'Allan Variance']
|
|
9
|
+
|
|
10
|
+
requires-python = ">= 3.10"
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy>=2.2.4",
|
|
14
|
+
"pyaml>=21.10.1",
|
|
15
|
+
"attrdict>=2.0.1",
|
|
16
|
+
"tqdm>=4.62.2",
|
|
17
|
+
"loguru>=0.6.0",
|
|
18
|
+
"matplotlib>=3.10.7",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
repository = "https://github.com/varunagrawal/allan_variance"
|
|
23
|
+
homepage = "https://github.com/varunagrawal/allan_variance"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=7.1.1", "pylint>=2.13.2"]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build >= 0.11.23"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[tool.uv.build-backend]
|
|
33
|
+
module-root = "."
|
|
34
|
+
module-name = "allan_variance"
|
|
35
|
+
source-exclude = ["config", "ros", "ros2", "simulator"]
|
|
36
|
+
|
|
37
|
+
namespaces = false
|