View Javadoc
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 }