View Javadoc
1   /*
2    * Copyright (C) 2016 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.ar.slam;
17  
18  import com.irurueta.algebra.ArrayUtils;
19  import com.irurueta.algebra.Matrix;
20  import com.irurueta.algebra.WrongSizeException;
21  import com.irurueta.numerical.signal.processing.MeasurementNoiseCovarianceEstimator;
22  import com.irurueta.numerical.signal.processing.SignalProcessingException;
23  import com.irurueta.statistics.InvalidCovarianceMatrixException;
24  import com.irurueta.statistics.MultivariateNormalDist;
25  
26  import java.util.Arrays;
27  
28  /**
29   * Base class for estimating mean and covariance of noise in control values
30   * when the system state is held constant (only noise is provided as control
31   * input).
32   *
33   * @param <D> type of calibration data.
34   */
35  @SuppressWarnings("DuplicatedCode")
36  public abstract class BaseSlamCalibrator<D extends BaseCalibrationData> {
37  
38      /**
39       * Minimum allowed sample length.
40       */
41      public static final int MIN_SAMPLE_LENGTH = 1;
42  
43      /**
44       * Default minimum number of samples to take into account.
45       */
46      public static final int DEFAULT_MIN_NUM_SAMPLES = 20;
47  
48      /**
49       * Default maximum number of samples to take into account.
50       */
51      public static final int DEFAULT_MAX_NUM_SAMPLES = 100;
52  
53      /**
54       * Value to consider that mean and covariance have converged.
55       */
56      public static final double DEFAULT_CONVERGENCE_THRESHOLD = 1e-5;
57  
58      /**
59       * Indicates whether sample accumulation must be enabled or not.
60       */
61      protected static final boolean DEFAULT_ENABLE_SAMPLE_ACCUMULATION = true;
62  
63      /**
64       * Number of components in 3D.
65       */
66      protected static final int N_COMPONENTS_3D = 3;
67  
68      /**
69       * Conversion of nanoseconds to milliseconds.
70       */
71      protected static final double NANOS_TO_SECONDS = 1e-9;
72  
73      /**
74       * Sample length of control values used during prediction stage in SLAM
75       * estimator.
76       */
77      private final int sampleLength;
78  
79      /**
80       * Array containing a control sample used during SLAM prediction stage.
81       */
82      protected double[] sample;
83  
84      /**
85       * Mean and covariance estimator.
86       */
87      protected MeasurementNoiseCovarianceEstimator estimator;
88  
89      /**
90       * Contains previous mean value.
91       */
92      protected double[] previousMean;
93  
94      /**
95       * Contains mean value of covariance.
96       */
97      protected Matrix previousCovariance;
98  
99      /**
100      * Indicates whether this calibrator converged.
101      */
102     protected boolean converged;
103 
104     /**
105      * Indicates whether this calibrator failed.
106      */
107     protected boolean failed;
108 
109     /**
110      * Indicates whether calibrator has finished taking samples.
111      */
112     protected boolean finished;
113 
114     /**
115      * Number of obtained samples.
116      */
117     protected int sampleCount;
118 
119     /**
120      * Array to store the difference between average values to determine whether
121      * the result has converged or not.
122      */
123     protected double[] meanDiff;
124 
125     /**
126      * Matrix to store the difference between covariance matrices to determine
127      * whether the result has converged or not.
128      */
129     protected Matrix covDiff;
130 
131     /**
132      * Minimum number of samples to take into account.
133      */
134     protected int minNumSamples = DEFAULT_MIN_NUM_SAMPLES;
135 
136     /**
137      * Maximum number of samples to take into account.
138      */
139     protected int maxNumSamples = DEFAULT_MAX_NUM_SAMPLES;
140 
141     /**
142      * Threshold to consider whether calibration has converged or not.
143      */
144     protected double convergenceThreshold = DEFAULT_CONVERGENCE_THRESHOLD;
145 
146     /**
147      * Indicates whether accumulation of samples is enabled or not.
148      */
149     protected boolean accumulationEnabled = DEFAULT_ENABLE_SAMPLE_ACCUMULATION;
150 
151     /**
152      * Timestamp expressed in nanoseconds since the epoch time of the last
153      * accelerometer sample.
154      */
155     protected long accelerometerTimestampNanos = -1;
156 
157     /**
158      * Timestamp expressed in nanoseconds since the epoch time of the last
159      * gyroscope sample.
160      */
161     protected long gyroscopeTimestampNanos = -1;
162 
163     /**
164      * Number of accelerometer samples accumulated since last full sample.
165      */
166     protected int accumulatedAccelerometerSamples = 0;
167 
168     /**
169      * Number of gyroscope samples accumulated since last full sample.
170      */
171     protected int accumulatedGyroscopeSamples = 0;
172 
173     /**
174      * Average of acceleration along x-axis accumulated since last full sample.
175      * Expressed in meters per squared second (m/s^2).
176      */
177     protected double accumulatedAccelerationSampleX;
178 
179     /**
180      * Average of acceleration along y-axis accumulated since last full sample.
181      * Expressed in meters per squared second (m/s^2).
182      */
183     protected double accumulatedAccelerationSampleY;
184 
185     /**
186      * Average of acceleration along z-axis accumulated since last full sample.
187      * Expressed in meters per squared second (m/s^2).
188      */
189     protected double accumulatedAccelerationSampleZ;
190 
191     /**
192      * Average of angular speed along x-axis accumulated since last full sample.
193      * Expressed in meters per squared second (m/s^2).
194      */
195     protected double accumulatedAngularSpeedSampleX;
196 
197     /**
198      * Average of angular speed along y-axis accumulated since last full sample.
199      * Expressed in meters per squared second (m/s^2).
200      */
201     protected double accumulatedAngularSpeedSampleY;
202 
203     /**
204      * Average of angular speed along z-axis accumulated since last full sample.
205      * Expressed in meters per squared second (m/s^2).
206      */
207     protected double accumulatedAngularSpeedSampleZ;
208 
209     /**
210      * Listener in charge of handling events raised by instances of this class.
211      */
212     protected BaseSlamCalibratorListener<D> listener;
213 
214     /**
215      * Constructor.
216      *
217      * @param sampleLength sample length of control values used during
218      *                     prediction stage in SLAM estimator.
219      * @throws IllegalArgumentException if sample length is less than 1.
220      */
221     protected BaseSlamCalibrator(final int sampleLength) {
222         if (sampleLength < MIN_SAMPLE_LENGTH) {
223             throw new IllegalArgumentException("length must be greater than 0");
224         }
225 
226         this.sampleLength = sampleLength;
227         sample = new double[sampleLength];
228         previousMean = new double[sampleLength];
229         meanDiff = new double[sampleLength];
230         try {
231             previousCovariance = new Matrix(sampleLength, sampleLength);
232             covDiff = new Matrix(sampleLength, sampleLength);
233             estimator = new MeasurementNoiseCovarianceEstimator(sampleLength);
234         } catch (final Exception ignore) {
235             // never thrown
236         }
237     }
238 
239     /**
240      * Gets sample length of control values used during prediction stage in SLAM
241      * estimator.
242      *
243      * @return sample length of control values used during prediction stage in
244      * SLAM estimator.
245      */
246     public int getSampleLength() {
247         return sampleLength;
248     }
249 
250     /**
251      * Indicates whether calibrator converged or not.
252      *
253      * @return true if calibrator converged, false otherwise.
254      */
255     public boolean isConverged() {
256         return converged;
257     }
258 
259     /**
260      * Indicates whether this calibrator failed or not.
261      *
262      * @return true if calibrator failed, false otherwise.
263      */
264     public boolean isFailed() {
265         return failed;
266     }
267 
268     /**
269      * Indicates whether calibrator has finished taking samples or not.
270      *
271      * @return true if calibrator has finished taking samples, false otherwise.
272      */
273     public boolean isFinished() {
274         return finished;
275     }
276 
277     /**
278      * Gets number of obtained samples.
279      *
280      * @return number of obtained samples.
281      */
282     public int getSampleCount() {
283         return sampleCount;
284     }
285 
286     /**
287      * Obtains the minimum number of samples to use before taking convergence
288      * into account.
289      *
290      * @return minimum number of samples to use before taking convergence into
291      * account.
292      */
293     public int getMinNumSamples() {
294         return minNumSamples;
295     }
296 
297     /**
298      * Specifies the minimum number of samples to take before taking convergence
299      * into account.
300      *
301      * @param minNumSamples minimum number of samples to take before taking
302      *                      convergence into account.
303      * @throws IllegalArgumentException if provided value is negative.
304      */
305     public void setMinNumSamples(final int minNumSamples) {
306         if (minNumSamples < 0) {
307             throw new IllegalArgumentException("minNumSamples must be positive");
308         }
309         this.minNumSamples = minNumSamples;
310     }
311 
312     /**
313      * Gets maximum number of samples to take into account.
314      *
315      * @return maximum number of samples to take into account.
316      */
317     public int getMaxNumSamples() {
318         return maxNumSamples;
319     }
320 
321     /**
322      * Specifies the maximum number of samples to take.
323      *
324      * @param maxNumSamples maximum number of samples to take.
325      * @throws IllegalArgumentException if provided value is negative or zero.
326      */
327     public void setMaxNumSamples(final int maxNumSamples) {
328         if (maxNumSamples <= 0) {
329             throw new IllegalArgumentException("maxNumSamples must be positive");
330         }
331         this.maxNumSamples = maxNumSamples;
332     }
333 
334     /**
335      * Gets threshold to consider that calibration has converged.
336      *
337      * @return threshold to consider that calibration has converged.
338      */
339     public double getConvergenceThreshold() {
340         return convergenceThreshold;
341     }
342 
343     /**
344      * Specifies threshold to determine that calibration has converged.
345      *
346      * @param convergenceThreshold threshold to determine that calibration has
347      *                             converged.
348      * @throws IllegalArgumentException if threshold is negative.
349      */
350     public void setConvergenceThreshold(final double convergenceThreshold) {
351         if (convergenceThreshold < 0.0) {
352             throw new IllegalArgumentException("convergenceThreshold must be positive");
353         }
354         this.convergenceThreshold = convergenceThreshold;
355     }
356 
357     /**
358      * Resets calibrator.
359      */
360     public void reset() {
361         converged = failed = false;
362         sampleCount = 0;
363         finished = false;
364         Arrays.fill(sample, 0.0);
365         Arrays.fill(previousMean, 0.0);
366         previousCovariance.initialize(0.0);
367         try {
368             estimator = new MeasurementNoiseCovarianceEstimator(sample.length);
369         } catch (final SignalProcessingException e) {
370             // never thrown
371         }
372         sampleCount = 0;
373         Arrays.fill(meanDiff, 0.0);
374         covDiff.initialize(0.0);
375         accelerometerTimestampNanos = gyroscopeTimestampNanos = -1;
376         accumulatedAccelerometerSamples = accumulatedGyroscopeSamples = 0;
377         accumulatedAccelerationSampleX = accumulatedAccelerationSampleY = accumulatedAccelerationSampleZ = 0.0;
378         accumulatedAngularSpeedSampleX = accumulatedAngularSpeedSampleY = accumulatedAngularSpeedSampleZ = 0.0;
379     }
380 
381     /**
382      * Indicates whether accumulation of samples is enabled or not.
383      *
384      * @return true if accumulation of samples is enabled, false otherwise.
385      */
386     public boolean isAccumulationEnabled() {
387         return accumulationEnabled;
388     }
389 
390     /**
391      * Specifies whether accumulation of samples is enabled or not.
392      *
393      * @param accumulationEnabled true if accumulation of samples is enabled,
394      *                            false otherwise.
395      */
396     public void setAccumulationEnabled(final boolean accumulationEnabled) {
397         this.accumulationEnabled = accumulationEnabled;
398     }
399 
400     /**
401      * Gets timestamp expressed in nanoseconds since the epoch time of the last
402      * accelerometer sample, or -1 if no sample has been set yet.
403      *
404      * @return timestamp expressed in nanoseconds since the epoch time of the
405      * last accelerometer sample, or -1.
406      */
407     public long getAccelerometerTimestampNanos() {
408         return accelerometerTimestampNanos;
409     }
410 
411     /**
412      * Gets timestamp expressed in nanoseconds since the epoch time of the last
413      * gyroscope sample, or -1 if no sample has been set yet.
414      *
415      * @return timestamp expressed in nanoseconds since the epoch time of the
416      * last gyroscope sample, or -1.
417      */
418     public long getGyroscopeTimestampNanos() {
419         return gyroscopeTimestampNanos;
420     }
421 
422     /**
423      * Gets number of accelerometer samples accumulated since last full sample.
424      *
425      * @return number of accelerometer samples accumulated since last full
426      * sample.
427      */
428     public int getAccumulatedAccelerometerSamples() {
429         return accumulatedAccelerometerSamples;
430     }
431 
432     /**
433      * Gets number of gyroscope samples accumulated since last full sample.
434      *
435      * @return number of gyroscope samples accumulated since last full sample.
436      */
437     public int getAccumulatedGyroscopeSamples() {
438         return accumulatedGyroscopeSamples;
439     }
440 
441     /**
442      * Indicates whether the accelerometer sample has been received since the
443      * last full sample (accelerometer + gyroscope).
444      *
445      * @return true if accelerometer sample has been received, false otherwise.
446      */
447     public boolean isAccelerometerSampleReceived() {
448         return accumulatedAccelerometerSamples > 0;
449     }
450 
451     /**
452      * Indicates whether the gyroscope sample has been received since the last
453      * full sample (accelerometer + gyroscope).
454      *
455      * @return true if gyroscope sample has been received, false otherwise.
456      */
457     public boolean isGyroscopeSampleReceived() {
458         return accumulatedGyroscopeSamples > 0;
459     }
460 
461     /**
462      * Indicates whether a full sample (accelerometer + gyroscope) has been
463      * received or not.
464      *
465      * @return true if full sample has been received, false otherwise.
466      */
467     public boolean isFullSampleAvailable() {
468         return isAccelerometerSampleReceived() && isGyroscopeSampleReceived();
469     }
470 
471     /**
472      * Gets average acceleration along x-axis accumulated since last full
473      * sample. Expressed in meters per squared second (m/s^2).
474      *
475      * @return average acceleration along x-axis accumulated since last full
476      * sample.
477      */
478     public double getAccumulatedAccelerationSampleX() {
479         return accumulatedAccelerationSampleX;
480     }
481 
482     /**
483      * Gets average acceleration along y-axis accumulated since last full
484      * sample. Expressed in meters per squared second (m/s^2).
485      *
486      * @return average acceleration along y-axis accumulated since last full
487      * sample.
488      */
489     public double getAccumulatedAccelerationSampleY() {
490         return accumulatedAccelerationSampleY;
491     }
492 
493     /**
494      * Gets average acceleration along z-axis accumulated since last full
495      * sample. Expressed in meters per squared second (m/s^2).
496      *
497      * @return average acceleration along z-axis accumulated since last full
498      * sample.
499      */
500     public double getAccumulatedAccelerationSampleZ() {
501         return accumulatedAccelerationSampleZ;
502     }
503 
504     /**
505      * Gets average acceleration along x,y,z axes accumulated since last full
506      * sample. Expressed in meters per squared second (m/s^2).
507      *
508      * @return average acceleration along x,y,z axes expressed in meters per
509      * squared second (m/s^2).
510      */
511     public double[] getAccumulatedAccelerationSample() {
512         return new double[]{
513                 accumulatedAccelerationSampleX,
514                 accumulatedAccelerationSampleY,
515                 accumulatedAccelerationSampleZ
516         };
517     }
518 
519     /**
520      * Gets average acceleration along x,yz axes accumulated since last full
521      * sample. Expressed in meters per squared second (m/s^2).
522      *
523      * @param result array where average acceleration along x,y,z axes will be
524      *               stored.
525      * @throws IllegalArgumentException if provided array does not have length
526      *                                  3.
527      */
528     public void getAccumulatedAccelerationSample(final double[] result) {
529         if (result.length != N_COMPONENTS_3D) {
530             throw new IllegalArgumentException("result must have length 3");
531         }
532         result[0] = accumulatedAccelerationSampleX;
533         result[1] = accumulatedAccelerationSampleY;
534         result[2] = accumulatedAccelerationSampleZ;
535     }
536 
537     /**
538      * Gets average angular speed along x-axis accumulated since last full
539      * sample. Expressed in radians per second (rad/s).
540      *
541      * @return average angular speed along x-axis expressed in radians per
542      * second (rad/s).
543      */
544     public double getAccumulatedAngularSpeedSampleX() {
545         return accumulatedAngularSpeedSampleX;
546     }
547 
548     /**
549      * Gets average angular speed along y-axis accumulated since last full
550      * sample. Expressed in radians per second (rad/s).
551      *
552      * @return average angular speed along y-axis expressed in radians per
553      * second (rad/s).
554      */
555     public double getAccumulatedAngularSpeedSampleY() {
556         return accumulatedAngularSpeedSampleY;
557     }
558 
559     /**
560      * Gets average angular speed along z-axis accumulated since last full
561      * sample. Expressed in radians per second (rad/s).
562      *
563      * @return average angular speed along z-axis expressed in radians per
564      * second (rad/s).
565      */
566     public double getAccumulatedAngularSpeedSampleZ() {
567         return accumulatedAngularSpeedSampleZ;
568     }
569 
570     /**
571      * Gets average angular speed along x,y,z axes accumulated since last full
572      * sample. Expressed in radians per second (rad/s).
573      *
574      * @return average angular speed along x,y,z axes expressed in radians per
575      * second.
576      */
577     public double[] getAccumulatedAngularSpeedSample() {
578         return new double[]{
579                 accumulatedAngularSpeedSampleX,
580                 accumulatedAngularSpeedSampleY,
581                 accumulatedAngularSpeedSampleZ
582         };
583     }
584 
585     /**
586      * Gets average angular speed along x,y,z axes accumulated since last full
587      * sample. Expressed in radians per second (rad/s).
588      *
589      * @param result array where average angular speed along x,y,z axes will be
590      *               stored.
591      * @throws IllegalArgumentException if provided array does not have length
592      *                                  3.
593      */
594     public void getAccumulatedAngularSpeedSample(final double[] result) {
595         if (result.length != N_COMPONENTS_3D) {
596             throw new IllegalArgumentException("result must have length 3");
597         }
598         result[0] = accumulatedAngularSpeedSampleX;
599         result[1] = accumulatedAngularSpeedSampleY;
600         result[2] = accumulatedAngularSpeedSampleZ;
601     }
602 
603     /**
604      * Provides a new accelerometer sample.
605      * If accumulation is enabled, samples are averaged until a full sample is
606      * received.
607      * When a full sample (accelerometer + gyroscope) is received, internal
608      * state gets also updated.
609      *
610      * @param timestamp     timestamp of accelerometer sample since epoch time and
611      * @param accelerationX linear acceleration along x-axis expressed in meters
612      *                      per squared second (m/s^2).
613      * @param accelerationY linear acceleration along y-axis expressed in meters
614      *                      per squared second (m/s^2).
615      * @param accelerationZ linear acceleration along z-axis expressed in meters
616      *                      per squared second (m/s^2).
617      */
618     public void updateAccelerometerSample(
619             final long timestamp, final float accelerationX, final float accelerationY, final float accelerationZ) {
620         if (!isFullSampleAvailable()) {
621             accelerometerTimestampNanos = timestamp;
622             if (isAccumulationEnabled() && isAccelerometerSampleReceived()) {
623                 // accumulation enabled
624                 final var nextSamples = accumulatedAccelerometerSamples + 1;
625                 accumulatedAccelerationSampleX = (accumulatedAccelerationSampleX * accumulatedAccelerometerSamples
626                         + accelerationX) / nextSamples;
627                 accumulatedAccelerationSampleY = (accumulatedAccelerationSampleY * accumulatedAccelerometerSamples
628                         + accelerationY) / nextSamples;
629                 accumulatedAccelerationSampleZ = (accumulatedAccelerationSampleZ * accumulatedAccelerometerSamples
630                         + accelerationZ) / nextSamples;
631                 accumulatedAccelerometerSamples = nextSamples;
632             } else {
633                 // accumulation disabled
634                 accumulatedAccelerationSampleX = accelerationX;
635                 accumulatedAccelerationSampleY = accelerationY;
636                 accumulatedAccelerationSampleZ = accelerationZ;
637                 accumulatedAccelerometerSamples++;
638             }
639             notifyFullSampleAndResetSampleReceive();
640         }
641     }
642 
643     /**
644      * Provides a new accelerometer sample.
645      * If accumulation is enabled, samples are averaged until a full sample is
646      * received.
647      * When a full sample (accelerometer + gyroscope) is received, internal
648      * state gets also updated.
649      *
650      * @param timestamp timestamp of accelerometer sample since epoch time and
651      *                  expressed in nanoseconds.
652      * @param data      array containing x,y,z components of linear acceleration
653      *                  expressed in meters per squared second (m/s^2).
654      * @throws IllegalArgumentException if provided array does not have length
655      *                                  3.
656      */
657     public void updateAccelerometerSample(final long timestamp, final float[] data) {
658         if (data.length != N_COMPONENTS_3D) {
659             throw new IllegalArgumentException("acceleration must have length 3");
660         }
661         updateAccelerometerSample(timestamp, data[0], data[1], data[2]);
662     }
663 
664     /**
665      * Provides a new gyroscope sample.
666      * If accumulation is enabled, samples are averaged until a full sample is
667      * received.
668      * When a full sample (accelerometer + gyroscope) is received, internal
669      * state gets also updated.
670      *
671      * @param timestamp     timestamp of accelerometer sample since epoch time and
672      *                      expressed in nanoseconds.
673      * @param angularSpeedX angular speed of rotation along x-axis expressed in
674      *                      radians per second (rad/s).
675      * @param angularSpeedY angular speed of rotation along y-axis expressed in
676      *                      radians per second (rad/s).
677      * @param angularSpeedZ angular speed of rotation along z-axis expressed in
678      *                      radians per second (rad/s).
679      */
680     public void updateGyroscopeSample(
681             final long timestamp, final float angularSpeedX, final float angularSpeedY, final float angularSpeedZ) {
682         if (!isFullSampleAvailable()) {
683             gyroscopeTimestampNanos = timestamp;
684             if (isAccumulationEnabled() && isGyroscopeSampleReceived()) {
685                 // accumulation enabled
686                 final var nextSamples = accumulatedGyroscopeSamples + 1;
687                 accumulatedAngularSpeedSampleX = (accumulatedAngularSpeedSampleX * accumulatedGyroscopeSamples
688                         + angularSpeedX) / nextSamples;
689                 accumulatedAngularSpeedSampleY = (accumulatedAngularSpeedSampleY * accumulatedGyroscopeSamples
690                         + angularSpeedY) / nextSamples;
691                 accumulatedAngularSpeedSampleZ = (accumulatedAngularSpeedSampleZ * accumulatedGyroscopeSamples
692                         + angularSpeedZ) / nextSamples;
693                 accumulatedGyroscopeSamples = nextSamples;
694             } else {
695                 // accumulation disabled
696                 accumulatedAngularSpeedSampleX = angularSpeedX;
697                 accumulatedAngularSpeedSampleY = angularSpeedY;
698                 accumulatedAngularSpeedSampleZ = angularSpeedZ;
699                 accumulatedGyroscopeSamples++;
700             }
701             notifyFullSampleAndResetSampleReceive();
702         }
703     }
704 
705     /**
706      * Provides a new gyroscope sample.
707      * If accumulation is enabled, samples are averaged until a full sample is
708      * received.
709      * When a full sample (accelerometer + gyroscope) is received, internal
710      * state gets also updated.
711      *
712      * @param timestamp timestamp of gyroscope sample since epoch time and
713      *                  expressed in nanoseconds.
714      * @param data      angular speed of rotation along x,y,z axes expressed in
715      *                  radians per second (rad/s).
716      * @throws IllegalArgumentException if provided array does not have length
717      *                                  3.
718      */
719     public void updateGyroscopeSample(final long timestamp, final float[] data) {
720         if (data.length != N_COMPONENTS_3D) {
721             throw new IllegalArgumentException("angular speed must have length 3");
722         }
723         updateGyroscopeSample(timestamp, data[0], data[1], data[2]);
724     }
725 
726     /**
727      * Gets most recent timestamp of received partial samples (accelerometer or
728      * gyroscope).
729      *
730      * @return most recent timestamp of received partial sample.
731      */
732     public long getMostRecentTimestampNanos() {
733         return Math.max(accelerometerTimestampNanos, gyroscopeTimestampNanos);
734     }
735 
736     /**
737      * Gets listener in charge of handling events raised by instances of this
738      * class.
739      *
740      * @return listener in charge of handling events raised by instances of this
741      * class.
742      */
743     public BaseSlamCalibratorListener<D> getListener() {
744         return listener;
745     }
746 
747     /**
748      * Sets listener in charge of handling events raised by instances of this
749      * class.
750      *
751      * @param listener listener in charge of handling events raised by instances
752      *                 of this class.
753      */
754     public void setListener(final BaseSlamCalibratorListener<D> listener) {
755         this.listener = listener;
756     }
757 
758     /**
759      * Obtains mean values of control signal used for SLAM estimation during
760      * prediction stage of Kalman filter in order to correct possible biases
761      * and offsets.
762      *
763      * @return mean values of control signal.
764      */
765     public double[] getControlMean() {
766         return estimator.getSampleAverage();
767     }
768 
769     /**
770      * Obtains mean values of control signal used for SLAM estimation during
771      * prediction stage of Kalman filter in order to correct possible biases
772      * and offsets.
773      *
774      * @param result array where mean values of control signal will be stored.
775      *               Array must have the same length as the control signal.
776      * @throws IllegalArgumentException if provided length is invalid.
777      */
778     public void getControlMean(final double[] result) {
779         final var src = getControlMean();
780         if (result.length != src.length) {
781             throw new IllegalArgumentException("wrong length");
782         }
783 
784         System.arraycopy(src, 0, result, 0, src.length);
785     }
786 
787     /**
788      * Gets covariance of control signal used for SLAM estimation during
789      * prediction stage of Kalman filter in order to correct possible biases
790      * and offsets.
791      *
792      * @return covariance matrix of control signal.
793      */
794     public Matrix getControlCovariance() {
795         return estimator.getMeasurementNoiseCov();
796     }
797 
798     /**
799      * Gets covariance of control signal used for SLAM estimation during
800      * prediction stage of Kalman filter in order to correct possible biases
801      * and offsets.
802      *
803      * @param result matrix where covariance will be stored.
804      */
805     public void getControlCovariance(final Matrix result) {
806         final var src = getControlCovariance();
807         src.copyTo(result);
808     }
809 
810     /**
811      * Gets a multivariate normal distribution containing control signal mean
812      * and covariance used for SLAM estimation during prediction stage of Kalman
813      * filter in order to correct possible biases and offsets.
814      *
815      * @return a multivariate normal distribution.
816      * @throws InvalidCovarianceMatrixException if estimated covariance is not
817      *                                          valid.
818      */
819     public MultivariateNormalDist getControlDistribution() throws InvalidCovarianceMatrixException {
820         final var cov = getControlCovariance();
821         try {
822             cov.symmetrize();
823         } catch (final WrongSizeException ignore) {
824             // never thrown
825         }
826         return new MultivariateNormalDist(getControlMean(), cov, false);
827     }
828 
829     /**
830      * Gets a multivariate normal distribution containing control signal mean
831      * and covariance used for SLAM estimation during prediction stage of Kalman
832      * filter in order to correct possible biases and offsets.
833      *
834      * @param dist a multivariate normal distribution.
835      * @throws InvalidCovarianceMatrixException if estimated covariance is not
836      *                                          valid.
837      */
838     public void getControlDistribution(final MultivariateNormalDist dist) throws InvalidCovarianceMatrixException {
839         final var cov = getControlCovariance();
840         try {
841             cov.symmetrize();
842         } catch (final WrongSizeException ignore) {
843             // never thrown
844         }
845         dist.setMeanAndCovariance(getControlMean(), cov, false);
846     }
847 
848     /**
849      * Gets a new instance containing calibration data estimated by this
850      * calibrator.
851      *
852      * @return a new calibration data instance.
853      */
854     public abstract D getCalibrationData();
855 
856     /**
857      * Gets calibration data estimated by this calibrator.
858      *
859      * @param result instance where calibration data will be stored.
860      */
861     public void getCalibrationData(final D result) {
862         result.setControlMeanAndCovariance(getControlMean(), getControlCovariance());
863     }
864 
865     /**
866      * Propagates calibrated control signal covariance using current control
867      * jacobian matrix.
868      * The propagated distribution can be used during prediction stage in Kalman
869      * filtering.
870      *
871      * @param controlJacobian current control jacobian matrix.
872      * @return propagated distribution.
873      * @throws InvalidCovarianceMatrixException if estimated covariance is not
874      *                                          valid.
875      */
876     public MultivariateNormalDist propagateWithControlJacobian(final Matrix controlJacobian)
877             throws InvalidCovarianceMatrixException {
878         return getCalibrationData().propagateWithControlJacobian(controlJacobian);
879     }
880 
881     /**
882      * Propagates calibrated control signal covariance using current control
883      * jacobian matrix.
884      * The propagated distribution can be used during prediction stage in Kalman
885      * filtering.
886      *
887      * @param controlJacobian current control jacobian matrix.
888      * @param result          instance where propagated distribution is stored.
889      * @throws InvalidCovarianceMatrixException if estimated covariance is not
890      *                                          valid.
891      */
892     public void propagateWithControlJacobian(final Matrix controlJacobian, final MultivariateNormalDist result)
893             throws InvalidCovarianceMatrixException {
894         getCalibrationData().propagateWithControlJacobian(controlJacobian, result);
895     }
896 
897     /**
898      * Notifies that a full sample has been received and resets flags indicating
899      * whether partial samples have been received.
900      */
901     protected void notifyFullSampleAndResetSampleReceive() {
902         if (isFullSampleAvailable()) {
903             processFullSample();
904             accumulatedAccelerometerSamples = accumulatedGyroscopeSamples = 0;
905         }
906     }
907 
908     /**
909      * Obtains the number of state parameters in associated SLAM estimator.
910      *
911      * @return number of state parameters.
912      */
913     protected abstract int getEstimatorStateLength();
914 
915     /**
916      * Method to be implemented in subclasses to process a full sample.
917      */
918     protected abstract void processFullSample();
919 
920     /**
921      * Updates internal mean and covariance values and checks whether
922      * convergence has been reached and calibrator has finished or failed.
923      */
924     protected void updateSample() {
925         if (finished) {
926             return;
927         }
928 
929         try {
930             estimator.update(sample);
931         } catch (final SignalProcessingException e) {
932             failed = finished = true;
933 
934             if (listener != null) {
935                 listener.onCalibratorFinished(this, converged, true);
936             }
937             return;
938         }
939 
940         sampleCount++;
941 
942         final var mean = estimator.getSampleAverage();
943         final var cov = estimator.getMeasurementNoiseCov();
944 
945         // check if minimum number of samples has been reached
946         if (sampleCount >= maxNumSamples) {
947             finished = true;
948 
949             if (listener != null) {
950                 listener.onCalibratorFinished(this, converged, failed);
951             }
952             return;
953         }
954 
955         // check if estimator has converged
956         if (sampleCount >= minNumSamples) {
957             ArrayUtils.subtract(mean, previousMean, meanDiff);
958             try {
959                 cov.subtract(previousCovariance, covDiff);
960             } catch (final WrongSizeException ignore) {
961                 // never thrown
962             }
963             final var meanDiffNorm = com.irurueta.algebra.Utils.normF(meanDiff);
964             final var covDiffNorm = com.irurueta.algebra.Utils.normF(covDiff);
965             if (meanDiffNorm <= convergenceThreshold && covDiffNorm <= convergenceThreshold) {
966                 converged = finished = true;
967 
968                 if (listener != null) {
969                     listener.onCalibratorFinished(this, true, failed);
970                 }
971                 return;
972             }
973         }
974 
975         // copy current value for next iteration
976         System.arraycopy(mean, 0, previousMean, 0, mean.length);
977         cov.copyTo(previousCovariance);
978 
979         finished = false;
980     }
981 
982     /**
983      * Listener for implementations of this class.
984      */
985     public interface BaseSlamCalibratorListener<D extends BaseCalibrationData> {
986         /**
987          * Called when a full sample (accelerometer + gyroscope, etc.) has been
988          * received.
989          *
990          * @param calibrator SLAM calibrator.
991          */
992         void onFullSampleReceived(final BaseSlamCalibrator<D> calibrator);
993 
994         /**
995          * Called when a full sample (accelerometer + gyroscope, etc.) has been
996          * received and has already been processed.
997          *
998          * @param calibrator SLAM calibrator.
999          */
1000         void onFullSampleProcessed(final BaseSlamCalibrator<D> calibrator);
1001 
1002         /**
1003          * Called when calibration finishes.
1004          *
1005          * @param calibrator SLAM calibrator.
1006          * @param converged  true if calibration converged, false otherwise.
1007          * @param failed     true if calibration failed, false otherwise.
1008          */
1009         void onCalibratorFinished(
1010                 final BaseSlamCalibrator<D> calibrator, final boolean converged, final boolean failed);
1011     }
1012 }