1 /*
2 * Copyright (C) 2020 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.navigation.inertial.calibration.noise;
17
18 import com.irurueta.navigation.LockedException;
19 import com.irurueta.navigation.inertial.calibration.TimeIntervalEstimator;
20 import com.irurueta.units.Measurement;
21 import com.irurueta.units.Time;
22 import com.irurueta.units.TimeConverter;
23 import com.irurueta.units.TimeUnit;
24
25 import java.util.Arrays;
26
27 /**
28 * Base class to estimate measurement noise variances and PSD's (Power Spectral Densities)
29 * along with average values for a windowed amount of samples.
30 * Implementations of this estimator may use norms of measurement triads to estimate noise
31 * levels.
32 * To compute PSD's, this estimator assumes that measurement samples are obtained
33 * at a constant provided rate equal to {@link #getTimeInterval()} seconds.
34 * If not available, accelerometer sampling rate average can be estimated using
35 * {@link TimeIntervalEstimator}.
36 * Notice that if there are less than {@link #getWindowSize()} processed
37 * samples in the window, this estimator will assume that the remaining ones
38 * until the window is completed have zero values.
39 *
40 * @param <U> a measurement unit type.
41 * @param <M> a measurement type.
42 * @param <E> an estimator type.
43 * @param <L> a listener type.
44 */
45 public abstract class WindowedMeasurementNoiseEstimator<U extends Enum<?>,
46 M extends Measurement<U>, E extends WindowedMeasurementNoiseEstimator<U, M, E, L>,
47 L extends WindowedMeasurementNoiseEstimatorListener<U, M, E>> {
48
49 /**
50 * Number of samples to keep within the window by default.
51 * For an accelerometer generating 100 samples/second, this is equivalent to
52 * 1 second.
53 * For an accelerometer generating 50 samples/second, this is equivalent to
54 * 2 seconds.
55 */
56 public static final int DEFAULT_WINDOW_SIZE = 101;
57
58 /**
59 * Minimum allowed window size.
60 */
61 public static final int MIN_WINDOW_SIZE = 3;
62
63 /**
64 * Default time interval between accelerometer samples expressed in seconds
65 * (s).
66 */
67 public static final double DEFAULT_TIME_INTERVAL_SECONDS = 0.02;
68
69 /**
70 * Length of number of samples to keep within the window being processed.
71 * Window size must always be larger than allowed minimum value and must
72 * have and odd value.
73 */
74 private int windowSize = DEFAULT_WINDOW_SIZE;
75
76 /**
77 * Time interval expressed in seconds (s) between consecutive measurements.
78 */
79 private double timeInterval = DEFAULT_TIME_INTERVAL_SECONDS;
80
81 /**
82 * Keeps the window of measurements expressed in their default units.
83 * (m/s^2 for acceleration, rad/s for angular speed or T for magnetic flux density).
84 */
85 private double[] windowedMeasurements = new double[DEFAULT_WINDOW_SIZE];
86
87 /**
88 * Listener to handle events raised by this estimator.
89 */
90 private L listener;
91
92 /**
93 * Contains estimated average of measurement expressed in its default unit
94 * (m/s^2 for acceleration, rad/s for angular speed or T for magnetic flux density).
95 */
96 private double avg;
97
98 /**
99 * Contains estimated variance of measurement expressed in its default squared unit
100 * (m^2/s^4 for acceleration, rad^2/s^2 for angular speed or T^2 for magnetic
101 * flux density).
102 */
103 private double variance;
104
105 /**
106 * Indicates position of first element in window.
107 */
108 private int firstCursor;
109
110 /**
111 * Indicates position of last element in window.
112 */
113 private int lastCursor;
114
115 /**
116 * Number of processed measurement samples.
117 */
118 private int numberOfProcessedSamples;
119
120 /**
121 * Number of added measurement samples.
122 */
123 private int numberOfAddedSamples;
124
125 /**
126 * Indicates whether estimator is running or not.
127 */
128 private boolean running;
129
130 /**
131 * Constructor.
132 */
133 protected WindowedMeasurementNoiseEstimator() {
134 }
135
136 /**
137 * Constructor.
138 *
139 * @param listener listener to handle events raised by this estimator.
140 */
141 protected WindowedMeasurementNoiseEstimator(final L listener) {
142 this.listener = listener;
143 }
144
145 /**
146 * Gets length of number of samples to keep within the window being processed.
147 * Window size must always be larger than allowed minimum value and must have
148 * and odd value.
149 *
150 * @return length of number of samples to keep within the window.
151 */
152 public int getWindowSize() {
153 return windowSize;
154 }
155
156 /**
157 * Sets length of number of samples to keep within the window being processed.
158 * Window size must always be larger than allowed minimum value and must have
159 * an odd value.
160 *
161 * @param windowSize length of number of samples to keep within the window.
162 * @throws IllegalArgumentException if provided value is not valid.
163 * @throws LockedException if estimator is currently running.
164 */
165 public void setWindowSize(final int windowSize) throws LockedException {
166 if (running) {
167 throw new LockedException();
168 }
169
170 // check that window is larger than minimum allowed value
171 if (windowSize < MIN_WINDOW_SIZE) {
172 throw new IllegalArgumentException();
173 }
174
175 // check that window size is not even
176 if (windowSize % 2 == 0) {
177 throw new IllegalArgumentException();
178 }
179
180 this.windowSize = windowSize;
181 windowedMeasurements = new double[windowSize];
182 reset();
183 }
184
185 /**
186 * Gets time interval between accelerometer triad samples expressed in
187 * seconds (s).
188 *
189 * @return time interval between accelerometer triad samples.
190 */
191 public double getTimeInterval() {
192 return timeInterval;
193 }
194
195 /**
196 * Sets time interval between accelerometer triad samples expressed in
197 * seconds (s).
198 *
199 * @param timeInterval time interval between accelerometer triad samples.
200 * @throws IllegalArgumentException if provided value is negative.
201 * @throws LockedException if estimator is currently running.
202 */
203 public void setTimeInterval(final double timeInterval) throws LockedException {
204 if (running) {
205 throw new LockedException();
206 }
207
208 if (timeInterval < 0.0) {
209 throw new IllegalArgumentException();
210 }
211
212 this.timeInterval = timeInterval;
213 }
214
215 /**
216 * Gets time interval between accelerometer triad samples.
217 *
218 * @return time interval between accelerometer triad samples.
219 */
220 public Time getTimeIntervalAsTime() {
221 return new Time(timeInterval, TimeUnit.SECOND);
222 }
223
224 /**
225 * Gets time interval between accelerometer triad samples.
226 *
227 * @param result instance where time interval will be stored.
228 */
229 public void getTimeIntervalAsTime(final Time result) {
230 result.setValue(timeInterval);
231 result.setUnit(TimeUnit.SECOND);
232 }
233
234 /**
235 * Sets time interval between accelerometer triad samples.
236 *
237 * @param timeInterval time interval between accelerometer triad samples.
238 * @throws LockedException if estimator is currently running.
239 */
240 public void setTimeInterval(final Time timeInterval) throws LockedException {
241 setTimeInterval(TimeConverter.convert(timeInterval.getValue().doubleValue(), timeInterval.getUnit(),
242 TimeUnit.SECOND));
243 }
244
245 /**
246 * Gets listener to handle events raised by this estimator.
247 *
248 * @return listener to handle events raised by this estimator.
249 */
250 public L getListener() {
251 return listener;
252 }
253
254 /**
255 * Sets listener to handle events raised by this estimator.
256 *
257 * @param listener listener to handle events raised by this estimator.
258 * @throws LockedException if this estimator is running.
259 */
260 public void setListener(final L listener) throws LockedException {
261 if (running) {
262 throw new LockedException();
263 }
264
265 this.listener = listener;
266 }
267
268 /**
269 * Gets first provided measurement value expressed in its default units.
270 * (m/s^2 for acceleration, rad/s for angular speed or T for magnetic flux density).
271 *
272 * @return first provided measurement value or null if not available.
273 */
274 public Double getFirstWindowedMeasurementValue() {
275 if (numberOfAddedSamples == 0) {
276 return null;
277 } else {
278 return windowedMeasurements[firstCursor % windowSize];
279 }
280 }
281
282 /**
283 * Gets las provided measurement value expressed in its default units.
284 * (m/s^2 for acceleration, rad/s for angular speed or T for magnetic flux density).
285 *
286 * @return last provided measurement value or null if not available.
287 */
288 public Double getLastWindowedMeasurementValue() {
289 if (numberOfAddedSamples == 0) {
290 return null;
291 } else {
292 return windowedMeasurements[(lastCursor - 1 + windowSize) % windowSize];
293 }
294 }
295
296 /**
297 * Gets first provided measurement within the window.
298 *
299 * @return first provided measurement within the window or null if not available.
300 */
301 public M getFirstWindowedMeasurement() {
302 final var value = getFirstWindowedMeasurementValue();
303 return value != null ? createMeasurement(value, getDefaultUnit()) : null;
304 }
305
306 /**
307 * Gets first provided measurement within the window.
308 *
309 * @param result instance where first provided measurement will be stored.
310 * @return true if result was updated, false if first measurement is not available.
311 */
312 public boolean getFirstWindowedMeasurement(final M result) {
313 final var value = getFirstWindowedMeasurementValue();
314 if (value != null) {
315 result.setValue(value);
316 result.setUnit(getDefaultUnit());
317 return true;
318 } else {
319 return false;
320 }
321 }
322
323 /**
324 * Gets last provided measurement within the window.
325 *
326 * @return last provided measurement within the window or null if not available.
327 */
328 public M getLastWindowedMeasurement() {
329 final var value = getLastWindowedMeasurementValue();
330 return value != null ? createMeasurement(value, getDefaultUnit()) : null;
331 }
332
333 /**
334 * Gets last provided measurement within the window.
335 *
336 * @param result instance where last provided measurement will be stored.
337 * @return true if result was updated, false if last measurement is not available.
338 */
339 public boolean getLastWindowedMeasurement(final M result) {
340 final var value = getLastWindowedMeasurementValue();
341 if (value != null) {
342 result.setValue(value);
343 result.setUnit(getDefaultUnit());
344 return true;
345 } else {
346 return false;
347 }
348 }
349
350 /**
351 * Gets estimated average of measurement expressed in its default unit
352 * (m/s^2 for acceleration, rad/s for angular speed or T for magnetic flux density).
353 *
354 * @return average of measurement in current window.
355 */
356 public double getAvg() {
357 return avg;
358 }
359
360 /**
361 * Gets estimated average of measurement within current window.
362 *
363 * @return average of measurement in current window
364 */
365 public M getAvgAsMeasurement() {
366 return createMeasurement(avg, getDefaultUnit());
367 }
368
369 /**
370 * Gets estimated average of measurement within current window.
371 *
372 * @param result instance where average of measurement will be stored.
373 */
374 public void getAvgAsMeasurement(final M result) {
375 result.setValue(avg);
376 result.setUnit(getDefaultUnit());
377 }
378
379 /**
380 * Gets estimated variance of measurement within current window
381 * expressed in its default squared unit (m^2/s^4 for acceleration,
382 * rad^2/s^2 for angular speed or T^2 for magnetic flux density).
383 *
384 * @return estimated variance of measurement within current window.
385 */
386 public double getVariance() {
387 return variance;
388 }
389
390 /**
391 * Gets estimated standard deviation of measurement within current window
392 * and expressed in its default unit (m/s^2 for acceleration, rad/s for
393 * angular speed or T for magnetic flux density).
394 *
395 * @return estimated standard of measurement.
396 */
397 public double getStandardDeviation() {
398 return Math.sqrt(variance);
399 }
400
401 /**
402 * Gets estimated standard deviation of measurement within current window.
403 *
404 * @return estimated standard deviation of measurement.
405 */
406 public M getStandardDeviationAsMeasurement() {
407 return createMeasurement(getStandardDeviation(), getDefaultUnit());
408 }
409
410 /**
411 * Gets estimated standard deviation of measurement within current window.
412 *
413 * @param result instance where estimated standard deviation of measurement
414 * will be stored.
415 */
416 public void getStandardDeviationAsMeasurement(final M result) {
417 result.setValue(getStandardDeviation());
418 result.setUnit(getDefaultUnit());
419 }
420
421 /**
422 * Gets measurement noise PSD (Power Spectral Density) expressed
423 * in (m^2 * s^-3) for accelerometer, (rad^2/s) for gyroscope or (T^2 * s) for
424 * magnetometer.
425 *
426 * @return measurement noise PSD.
427 */
428 public double getPsd() {
429 return variance * timeInterval;
430 }
431
432 /**
433 * Gets measurement noise root PSD (Power Spectral Density) expressed in
434 * (m * s^-1.5) for accelerometer, (rad * s^-0.5) for gyroscope or (T * s^0.5) for
435 * magnetometer.
436 *
437 * @return measurement noise root PSD.
438 */
439 public double getRootPsd() {
440 return Math.sqrt(getPsd());
441 }
442
443 /**
444 * Gets number of samples that have been processed so far.
445 *
446 * @return number of samples that have been processed so far.
447 */
448 public int getNumberOfProcessedSamples() {
449 return numberOfProcessedSamples;
450 }
451
452 /**
453 * Gets number of samples that have been added so far.
454 *
455 * @return number of samples that have been added so far.
456 */
457 public int getNumberOfAddedSamples() {
458 return numberOfAddedSamples;
459 }
460
461 /**
462 * Gets number of currently windowed samples.
463 *
464 * @return number of samples within the window.
465 */
466 public int getNumberOfSamplesInWindow() {
467 return Math.min(numberOfAddedSamples, windowSize);
468 }
469
470 /**
471 * Indicates whether window of samples is filled or not.
472 *
473 * @return true if window is filled, false otherwise.
474 */
475 public boolean isWindowFilled() {
476 return getNumberOfSamplesInWindow() == windowSize;
477 }
478
479 /**
480 * Indicates whether estimator is currently running or not.
481 *
482 * @return true if estimator is running, false otherwise.
483 */
484 public boolean isRunning() {
485 return running;
486 }
487
488 /**
489 * Adds a measurement value expressed in its default unit (m/s^2 for acceleration, rad/s for
490 * angular speed or T for magnetic flux density) and processes current window.
491 * Notice that if there are less than {@link #getWindowSize()} processed
492 * samples in the window, the remaining ones are considered to be zero
493 * when average values and standard deviation is estimated.
494 *
495 * @param value value to be added.
496 * @throws LockedException if estimator is currently running.
497 */
498 public void addMeasurementAndProcess(final double value) throws LockedException {
499 internalAdd(value, true);
500 }
501
502 /**
503 * Adds a measurement and processes current window.
504 * Notice that if there are less than {@link #getWindowSize()} processed
505 * samples in the window, the remaining ones are considered to be zero
506 * when average values and standard deviation is estimated.
507 *
508 * @param value value to be added.
509 * @throws LockedException if estimator is currently running.
510 */
511 public void addMeasurementAndProcess(final M value) throws LockedException {
512 internalAdd(convertToDefaultUnit(value), true);
513 }
514
515 /**
516 * Adds a measurement value expressed in its default unit (m/s^2 for acceleration, rad/s for
517 * angular speed or T for magnetic flux density).
518 * Notice that if there are less than {@link #getWindowSize()} processed
519 * samples in the window, the remaining ones are considered to be zero
520 * when average values and standard deviation is estimated.
521 *
522 * @param value value to be added.
523 * @throws LockedException if estimator is currently running.
524 */
525 public void addMeasurement(final double value) throws LockedException {
526 internalAdd(value, false);
527 }
528
529 /**
530 * Adds a measurement.
531 * Notice that if there are less than {@link #getWindowSize()} processed
532 * samples in the window, the remaining ones are considered to be zero
533 * when average values and standard deviation is estimated.
534 *
535 * @param value value to be added.
536 * @throws LockedException if estimator is currently running
537 */
538 public void addMeasurement(final M value) throws LockedException {
539 internalAdd(convertToDefaultUnit(value), false);
540 }
541
542 /**
543 * Resets current estimator.
544 *
545 * @return true if estimator was successfully reset, false if no reset was needed.
546 * @throws LockedException if estimator is currently running.
547 */
548 public boolean reset() throws LockedException {
549 if (running) {
550 throw new LockedException();
551 }
552
553 if (numberOfProcessedSamples == 0) {
554 return false;
555 }
556
557 Arrays.fill(windowedMeasurements, 0.0);
558 firstCursor = 0;
559 lastCursor = 0;
560 avg = 0.0;
561 variance = 0.0;
562 numberOfProcessedSamples = 0;
563 numberOfAddedSamples = 0;
564
565 if (listener != null) {
566 //noinspection unchecked
567 listener.onReset((E) this);
568 }
569
570 return true;
571 }
572
573 /**
574 * Gets default unit for a measurement.
575 *
576 * @return default unit for a measurement.
577 */
578 protected abstract U getDefaultUnit();
579
580 /**
581 * Creates a measurement with provided value and unit.
582 *
583 * @param value value to be set.
584 * @param unit unit to be set.
585 * @return created measurement.
586 */
587 protected abstract M createMeasurement(final double value, final U unit);
588
589 /**
590 * Converts provided measurement into default unit.
591 *
592 * @param value measurement to be converted.
593 * @return converted value.
594 */
595 protected abstract double convertToDefaultUnit(M value);
596
597 /**
598 * Internally adds a measurement value and processes current window if indicated.
599 *
600 * @param value measurement value to be added.
601 * @param process true if window of samples must also be processed, false otherwise.
602 * @throws LockedException if estimator is currently running.
603 */
604 private void internalAdd(final double value, final boolean process) throws LockedException {
605 if (running) {
606 throw new LockedException();
607 }
608
609 running = true;
610
611 if (numberOfAddedSamples == 0 && listener != null) {
612 //noinspection unchecked
613 listener.onStart((E) this);
614 }
615
616 final var wasFilled = isWindowFilled();
617 if (wasFilled) {
618 // increase first cursor
619 firstCursor = (firstCursor + 1) % windowSize;
620 }
621 windowedMeasurements[lastCursor] = value;
622 lastCursor = (lastCursor + 1) % windowSize;
623 numberOfAddedSamples++;
624
625 // process window
626 if (process) {
627 processWindow();
628 }
629
630 running = false;
631
632 if (listener != null) {
633 //noinspection unchecked
634 listener.onMeasurementAdded((E) this);
635
636 if (!wasFilled && isWindowFilled()) {
637 //noinspection unchecked
638 listener.onWindowFilled((E) this);
639 }
640 }
641 }
642
643 /**
644 * Processes current windowed samples.
645 */
646 private void processWindow() {
647 numberOfProcessedSamples++;
648
649 final var n = getNumberOfSamplesInWindow();
650
651 final var endPos = Math.min(n, windowSize);
652
653 // compute averages
654 var localAverage = 0.0;
655 for (var i = 0; i < endPos; i++) {
656 final var value = windowedMeasurements[i];
657 localAverage += value;
658 }
659
660 localAverage /= windowSize;
661
662 // compute variances
663 var localVariance = 0.0;
664 for (var i = 0; i < endPos; i++) {
665 final var value = windowedMeasurements[i];
666 final var diff = value - localAverage;
667 final var diff2 = diff * diff;
668
669 localVariance += diff2;
670 }
671
672 final var m = windowSize - 1;
673
674 localVariance /= m;
675
676 this.avg = localAverage;
677 this.variance = localVariance;
678 }
679 }