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.
@@ -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,3 @@
1
+ from allan_variance.base import *
2
+ from allan_variance.imu_data import *
3
+ from allan_variance.rosbag_reader import *
@@ -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