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.accelerometer;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.algebra.Matrix;
20  import com.irurueta.algebra.Utils;
21  import com.irurueta.algebra.WrongSizeException;
22  import com.irurueta.navigation.LockedException;
23  import com.irurueta.navigation.NotReadyException;
24  import com.irurueta.navigation.inertial.BodyKinematics;
25  import com.irurueta.navigation.inertial.calibration.AccelerationTriad;
26  import com.irurueta.navigation.inertial.calibration.AccelerometerCalibrationSource;
27  import com.irurueta.navigation.inertial.calibration.CalibrationException;
28  import com.irurueta.navigation.inertial.calibration.FrameBodyKinematics;
29  import com.irurueta.navigation.inertial.estimators.ECEFKinematicsEstimator;
30  import com.irurueta.units.Acceleration;
31  import com.irurueta.units.AccelerationUnit;
32  
33  import java.util.Collection;
34  
35  /**
36   * Estimates accelerometer biases, cross couplings and scaling factors.
37   * <p>
38   * This calibrator uses a linear approach to find a minimum least squared error
39   * solution.
40   * <p>
41   * To use this calibrator at least 4 measurements at different known frames must
42   * be provided. In other words, accelerometer samples must be obtained at 4
43   * different positions, orientations and velocities (although typically velocities are
44   * always zero).
45   * <p>
46   * Measured specific force is assumed to follow the model shown below:
47   * <pre>
48   *     fmeas = ba + (I + Ma) * ftrue + w
49   * </pre>
50   * Where:
51   * - fmeas is the measured specific force. This is a 3x1 vector.
52   * - ba is accelerometer bias. Ideally, on a perfect accelerometer, this should be a
53   * 3x1 zero vector.
54   * - I is the 3x3 identity matrix.
55   * - Ma is the 3x3 matrix containing cross-couplings and scaling factors. Ideally, on
56   * a perfect accelerometer, this should be a 3x3 zero matrix.
57   * - ftrue is ground-truth specific force. This is a 3x1 vector.
58   * - w is measurement noise. This is a 3x1 vector.
59   */
60  @SuppressWarnings("DuplicatedCode")
61  public class KnownFrameAccelerometerLinearLeastSquaresCalibrator implements
62          KnownFrameAccelerometerCalibrator<FrameBodyKinematics,
63                  KnownFrameAccelerometerLinearLeastSquaresCalibratorListener>, AccelerometerCalibrationSource,
64          UnorderedFrameBodyKinematicsAccelerometerCalibrator {
65  
66      /**
67       * Indicates whether by default a common z-axis is assumed for both the accelerometer
68       * and gyroscope.
69       */
70      public static final boolean DEFAULT_USE_COMMON_Z_AXIS = false;
71  
72      /**
73       * Required minimum number of measurements.
74       */
75      public static final int MINIMUM_MEASUREMENTS = 4;
76  
77      /**
78       * Number of equations generated for each measurement.
79       */
80      private static final int EQUATIONS_PER_MEASUREMENT = 3;
81  
82      /**
83       * Number of unknowns when common z-axis is assumed for both the accelerometer
84       * and gyroscope.
85       */
86      private static final int COMMON_Z_AXIS_UNKNOWNS = 9;
87  
88      /**
89       * Number of unknowns for the general case.
90       */
91      private static final int GENERAL_UNKNOWNS = 12;
92  
93      /**
94       * Contains a collection of body kinematics measurements taken at different
95       * frames (positions, orientations and velocities).
96       * If a single device IMU needs to be calibrated, typically all measurements are
97       * taken at the same position, with zero velocity and multiple orientations.
98       * However, if we just want to calibrate a given IMU model (e.g. obtain
99       * an average and less precise calibration for the IMU of a given phone model),
100      * we could take measurements collected throughout the planet at multiple positions
101      * while the phone remains static (e.g. while charging), hence each measurement
102      * position will change, velocity will remain zero and orientation will be
103      * typically constant at horizontal orientation while the phone remains on a
104      * flat surface.
105      */
106     private Collection<FrameBodyKinematics> measurements;
107 
108     /**
109      * This flag indicates whether z-axis is assumed to be common for accelerometer
110      * and gyroscope.
111      * When enabled, this eliminates 3 variables from Ma matrix.
112      */
113     private boolean commonAxisUsed = DEFAULT_USE_COMMON_Z_AXIS;
114 
115     /**
116      * Listener to handle events raised by this calibrator.
117      */
118     private KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener;
119 
120     /**
121      * Estimated accelerometer biases for each IMU axis expressed in meter per squared
122      * second (m/s^2).
123      */
124     private double[] estimatedBiases;
125 
126     /**
127      * Estimated accelerometer scale factors and cross coupling errors.
128      * This is the product of matrix Ta containing cross coupling errors and Ka
129      * containing scaling factors.
130      * So tat:
131      * <pre>
132      *     Ma = [sx    mxy  mxz] = Ta*Ka
133      *          [myx   sy   myz]
134      *          [mzx   mzy  sz ]
135      * </pre>
136      * Where:
137      * <pre>
138      *     Ka = [sx 0   0 ]
139      *          [0  sy  0 ]
140      *          [0  0   sz]
141      * </pre>
142      * and
143      * <pre>
144      *     Ta = [1          -alphaXy    alphaXz ]
145      *          [alphaYx    1           -alphaYz]
146      *          [-alphaZx   alphaZy     1       ]
147      * </pre>
148      * Hence:
149      * <pre>
150      *     Ma = [sx    mxy  mxz] = Ta*Ka =  [sx             -sy * alphaXy   sz * alphaXz ]
151      *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
152      *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
153      * </pre>
154      * This instance allows any 3x3 matrix however, typically alphaYx, alphaZx and alphaZy
155      * are considered to be zero if the accelerometer z-axis is assumed to be the same
156      * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Ma matrix
157      * becomes upper diagonal:
158      * <pre>
159      *     Ma = [sx    mxy  mxz]
160      *          [0     sy   myz]
161      *          [0     0    sz ]
162      * </pre>
163      * Values of this matrix are unit-less.
164      */
165     private Matrix estimatedMa;
166 
167     /**
168      * Indicates whether calibrator is running.
169      */
170     private boolean running;
171 
172     /**
173      * Constructor.
174      */
175     public KnownFrameAccelerometerLinearLeastSquaresCalibrator() {
176     }
177 
178     /**
179      * Constructor.
180      *
181      * @param listener listener to handle events raised by this calibrator.
182      */
183     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
184             final KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener) {
185         this.listener = listener;
186     }
187 
188     /**
189      * Constructor.
190      *
191      * @param measurements collection of body kinematics measurements taken at
192      *                     different frames (positions, orientations and velocities).
193      */
194     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
195             final Collection<? extends FrameBodyKinematics> measurements) {
196         //noinspection unchecked
197         this.measurements = (Collection<FrameBodyKinematics>) measurements;
198     }
199 
200     /**
201      * Constructor.
202      *
203      * @param measurements collection of body kinematics measurements taken at
204      *                     different frames (positions, orientations and velocities).
205      * @param listener     listener to handle events raised by this calibrator.
206      */
207     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
208             final Collection<? extends FrameBodyKinematics> measurements,
209             final KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener) {
210         this(measurements);
211         this.listener = listener;
212     }
213 
214     /**
215      * Constructor.
216      *
217      * @param commonAxisUsed indicates whether z-axis is assumed to be common for
218      *                       accelerometer and gyroscope.
219      */
220     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(final boolean commonAxisUsed) {
221         this.commonAxisUsed = commonAxisUsed;
222     }
223 
224     /**
225      * Constructor.
226      *
227      * @param commonAxisUsed indicates whether z-axis is assumed to be common for
228      *                       accelerometer and gyroscope.
229      * @param listener       listener to handle events raised by this calibrator.
230      */
231     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
232             final boolean commonAxisUsed, final KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener) {
233         this(commonAxisUsed);
234         this.listener = listener;
235     }
236 
237     /**
238      * Constructor.
239      *
240      * @param measurements   collection of body kinematics measurements taken at
241      *                       different frames (positions, orientations and velocities).
242      * @param commonAxisUsed indicates whether z-axis is assumed to be common for
243      *                       accelerometer and gyroscope.
244      */
245     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
246             final Collection<? extends FrameBodyKinematics> measurements, final boolean commonAxisUsed) {
247         this(measurements);
248         this.commonAxisUsed = commonAxisUsed;
249     }
250 
251     /**
252      * Constructor.
253      *
254      * @param measurements   collection of body kinematics measurements taken at
255      *                       different frames (positions, orientations and velocities).
256      * @param commonAxisUsed indicates whether z-axis is assumed to be common for
257      *                       accelerometer and gyroscope.
258      * @param listener       listener to handle events raised by this calibrator.
259      */
260     public KnownFrameAccelerometerLinearLeastSquaresCalibrator(
261             final Collection<? extends FrameBodyKinematics> measurements, final boolean commonAxisUsed,
262             final KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener) {
263         this(measurements, commonAxisUsed);
264         this.listener = listener;
265     }
266 
267     /**
268      * Gets a collection of body kinematics measurements taken at different
269      * frames (positions, orientations and velocities).
270      * If a single device IMU needs to be calibrated, typically all measurements are
271      * taken at the same position, with zero velocity and multiple orientations.
272      * However, if we just want to calibrate a given IMU model (e.g. obtain
273      * an average and less precise calibration for the IMU of a given phone model),
274      * we could take measurements collected throughout the planet at multiple positions
275      * while the phone remains static (e.g. while charging), hence each measurement
276      * position will change, velocity will remain zero and orientation will be
277      * typically constant at horizontal orientation while the phone remains on a
278      * flat surface.
279      *
280      * @return a collection of body kinematics measurements taken at different
281      * frames (positions, orientations and velocities).
282      */
283     @Override
284     public Collection<FrameBodyKinematics> getMeasurements() {
285         return measurements;
286     }
287 
288     /**
289      * Sets a collection of body kinematics measurements taken at different
290      * frames (positions, orientations and velocities).
291      * If a single device IMU needs to be calibrated, typically all measurements are
292      * taken at the same position, with zero velocity and multiple orientations.
293      * However, if we just want to calibrate the a given IMU model (e.g. obtain
294      * an average and less precise calibration for the IMU of a given phone model),
295      * we could take measurements collected throughout the planet at multiple positions
296      * while the phone remains static (e.g. while charging), hence each measurement
297      * position will change, velocity will remain zero and orientation will be
298      * typically constant at horizontal orientation while the phone remains on a
299      * flat surface.
300      *
301      * @param measurements collection of body kinematics measurements taken at different
302      *                     frames (positions, orientations and velocities).
303      * @throws LockedException if calibrator is currently running.
304      */
305     @Override
306     public void setMeasurements(final Collection<? extends FrameBodyKinematics> measurements) throws LockedException {
307         if (running) {
308             throw new LockedException();
309         }
310         //noinspection unchecked
311         this.measurements = (Collection<FrameBodyKinematics>) measurements;
312     }
313 
314     /**
315      * Indicates the type of measurement used by this calibrator.
316      *
317      * @return type of measurement used by this calibrator.
318      */
319     @Override
320     public AccelerometerCalibratorMeasurementType getMeasurementType() {
321         return AccelerometerCalibratorMeasurementType.FRAME_BODY_KINEMATICS;
322     }
323 
324     /**
325      * Indicates whether this calibrator requires ordered measurements in a
326      * list or not.
327      *
328      * @return true if measurements must be ordered, false otherwise.
329      */
330     @Override
331     public boolean isOrderedMeasurementsRequired() {
332         return false;
333     }
334 
335     /**
336      * Indicates whether this calibrator requires quality scores for each
337      * measurement or not.
338      *
339      * @return true if quality scores are required, false otherwise.
340      */
341     @Override
342     public boolean isQualityScoresRequired() {
343         return false;
344     }
345 
346     /**
347      * Indicates whether z-axis is assumed to be common for accelerometer and
348      * gyroscope.
349      * When enabled, this eliminates 3 variables from Ma matrix.
350      *
351      * @return true if z-axis is assumed to be common for accelerometer and gyroscope,
352      * false otherwise.
353      */
354     @Override
355     public boolean isCommonAxisUsed() {
356         return commonAxisUsed;
357     }
358 
359     /**
360      * Specifies whether z-axis is assumed to be common for accelerometer and
361      * gyroscope.
362      * When enabled, this eliminates 3 variables from Ma matrix.
363      *
364      * @param commonAxisUsed true if z-axis is assumed to be common for accelerometer
365      *                       and gyroscope, false otherwise.
366      * @throws LockedException if calibrator is currently running.
367      */
368     @Override
369     public void setCommonAxisUsed(final boolean commonAxisUsed) throws LockedException {
370         if (running) {
371             throw new LockedException();
372         }
373 
374         this.commonAxisUsed = commonAxisUsed;
375     }
376 
377     /**
378      * Gets listener to handle events raised by this calibrator.
379      *
380      * @return listener to handle events raised by this calibrator.
381      */
382     @Override
383     public KnownFrameAccelerometerLinearLeastSquaresCalibratorListener getListener() {
384         return listener;
385     }
386 
387     /**
388      * Sets listener to handle events raised by this calibrator.
389      *
390      * @param listener listener to handle events raised by this calibrator.
391      * @throws LockedException if calibrator is currently running.
392      */
393     @Override
394     public void setListener(
395             final KnownFrameAccelerometerLinearLeastSquaresCalibratorListener listener) throws LockedException {
396         if (running) {
397             throw new LockedException();
398         }
399 
400         this.listener = listener;
401     }
402 
403     /**
404      * Gets minimum number of required measurements.
405      *
406      * @return minimum number of required measurements.
407      */
408     @Override
409     public int getMinimumRequiredMeasurements() {
410         return MINIMUM_MEASUREMENTS;
411     }
412 
413     /**
414      * Indicates whether calibrator is ready to start the calibration.
415      *
416      * @return true if calibrator is ready, false otherwise.
417      */
418     @Override
419     public boolean isReady() {
420         return measurements != null && measurements.size() >= MINIMUM_MEASUREMENTS;
421     }
422 
423     /**
424      * Indicates whether calibrator is currently running or not.
425      *
426      * @return true if calibrator is running, false otherwise.
427      */
428     @Override
429     public boolean isRunning() {
430         return running;
431     }
432 
433     /**
434      * Estimates accelerometer calibration parameters containing bias, scale factors
435      * and cross-coupling errors.
436      *
437      * @throws LockedException      if calibrator is currently running.
438      * @throws NotReadyException    if calibrator is not ready.
439      * @throws CalibrationException if calibration fails for numerical reasons.
440      */
441     @Override
442     public void calibrate() throws LockedException, NotReadyException, CalibrationException {
443         if (running) {
444             throw new LockedException();
445         }
446 
447         if (!isReady()) {
448             throw new NotReadyException();
449         }
450 
451         try {
452             running = true;
453 
454             if (listener != null) {
455                 listener.onCalibrateStart(this);
456             }
457 
458             if (commonAxisUsed) {
459                 calibrateCommonAxis();
460             } else {
461                 calibrateGeneral();
462             }
463 
464             if (listener != null) {
465                 listener.onCalibrateEnd(this);
466             }
467 
468         } catch (final AlgebraException e) {
469             throw new CalibrationException(e);
470         } finally {
471             running = false;
472         }
473     }
474 
475     /**
476      * Gets array containing x,y,z components of estimated accelerometer biases
477      * expressed in meters per squared second (m/s^2).
478      *
479      * @return array containing x,y,z components of estimated accelerometer biases.
480      */
481     @Override
482     public double[] getEstimatedBiases() {
483         return estimatedBiases;
484     }
485 
486     /**
487      * Gets array containing x,y,z components of estimated accelerometer biases
488      * expressed in meters per squared second (m/s^2).
489      *
490      * @param result instance where estimated accelerometer biases will be stored.
491      * @return true if result instance was updated, false otherwise (when estimation
492      * is not yet available).
493      */
494     @Override
495     public boolean getEstimatedBiases(final double[] result) {
496         if (estimatedBiases != null) {
497             System.arraycopy(estimatedBiases, 0, result, 0, estimatedBiases.length);
498             return true;
499         } else {
500             return false;
501         }
502     }
503 
504     /**
505      * Gets column matrix containing x,y,z components of estimated accelerometer biases
506      * expressed in meters per squared second (m/s^2).
507      *
508      * @return column matrix containing x,y,z components of estimated accelerometer
509      * biases.
510      */
511     @Override
512     public Matrix getEstimatedBiasesAsMatrix() {
513         return estimatedBiases != null ? Matrix.newFromArray(estimatedBiases) : null;
514     }
515 
516     /**
517      * Gets column matrix containing x,y,z components of estimated accelerometer biases
518      * expressed in meters per squared second (m/s^2).
519      *
520      * @param result instance where result data will be stored.
521      * @return true if result was updated, false otherwise.
522      * @throws WrongSizeException if provided result instance has invalid size.
523      */
524     @Override
525     public boolean getEstimatedBiasesAsMatrix(final Matrix result) throws WrongSizeException {
526         if (estimatedBiases != null) {
527             result.fromArray(estimatedBiases);
528             return true;
529         } else {
530             return false;
531         }
532     }
533 
534     /**
535      * Gets x coordinate of estimated accelerometer bias expressed in meters per
536      * squared second (m/s^2).
537      *
538      * @return x coordinate of estimated accelerometer bias or null if not available.
539      */
540     @Override
541     public Double getEstimatedBiasFx() {
542         return estimatedBiases != null ? estimatedBiases[0] : null;
543     }
544 
545     /**
546      * Gets y coordinate of estimated accelerometer bias expressed in meters per
547      * squared second (m/s^2).
548      *
549      * @return y coordinate of estimated accelerometer bias or null if not available.
550      */
551     @Override
552     public Double getEstimatedBiasFy() {
553         return estimatedBiases != null ? estimatedBiases[1] : null;
554     }
555 
556     /**
557      * Gets z coordinate of estimated accelerometer bias expressed in meters per
558      * squared second (m/s^2).
559      *
560      * @return z coordinate of estimated accelerometer bias or null if not available.
561      */
562     @Override
563     public Double getEstimatedBiasFz() {
564         return estimatedBiases != null ? estimatedBiases[2] : null;
565     }
566 
567     /**
568      * Gets x coordinate of estimated accelerometer bias.
569      *
570      * @return x coordinate of estimated accelerometer bias or null if not available.
571      */
572     @Override
573     public Acceleration getEstimatedBiasFxAsAcceleration() {
574         return estimatedBiases != null
575                 ? new Acceleration(estimatedBiases[0], AccelerationUnit.METERS_PER_SQUARED_SECOND) : null;
576     }
577 
578     /**
579      * Gets x coordinate of estimated accelerometer bias.
580      *
581      * @param result instance where result will be stored.
582      * @return true if result was updated, false if estimation is not available.
583      */
584     @Override
585     public boolean getEstimatedBiasFxAsAcceleration(final Acceleration result) {
586         if (estimatedBiases != null) {
587             result.setValue(estimatedBiases[0]);
588             result.setUnit(AccelerationUnit.METERS_PER_SQUARED_SECOND);
589             return true;
590         } else {
591             return false;
592         }
593     }
594 
595     /**
596      * Gets y coordinate of estimated accelerometer bias.
597      *
598      * @return y coordinate of estimated accelerometer bias or null if not available.
599      */
600     @Override
601     public Acceleration getEstimatedBiasFyAsAcceleration() {
602         return estimatedBiases != null
603                 ? new Acceleration(estimatedBiases[1], AccelerationUnit.METERS_PER_SQUARED_SECOND) : null;
604     }
605 
606     /**
607      * Gets y coordinate of estimated accelerometer bias.
608      *
609      * @param result instance where result will be stored.
610      * @return true if result was updated, false if estimation is not available.
611      */
612     @Override
613     public boolean getEstimatedBiasFyAsAcceleration(final Acceleration result) {
614         if (estimatedBiases != null) {
615             result.setValue(estimatedBiases[1]);
616             result.setUnit(AccelerationUnit.METERS_PER_SQUARED_SECOND);
617             return true;
618         } else {
619             return false;
620         }
621     }
622 
623     /**
624      * Gets z coordinate of estimated accelerometer bias.
625      *
626      * @return z coordinate of estimated accelerometer bias or null if not available.
627      */
628     @Override
629     public Acceleration getEstimatedBiasFzAsAcceleration() {
630         return estimatedBiases != null
631                 ? new Acceleration(estimatedBiases[2], AccelerationUnit.METERS_PER_SQUARED_SECOND) : null;
632     }
633 
634     /**
635      * Gets z coordinate of estimated accelerometer bias.
636      *
637      * @param result instance where result will be stored.
638      * @return true if result was updated, false if estimation is not available.
639      */
640     @Override
641     public boolean getEstimatedBiasFzAsAcceleration(final Acceleration result) {
642         if (estimatedBiases != null) {
643             result.setValue(estimatedBiases[2]);
644             result.setUnit(AccelerationUnit.METERS_PER_SQUARED_SECOND);
645             return true;
646         } else {
647             return false;
648         }
649     }
650 
651     /**
652      * Gets estimated accelerometer bias.
653      *
654      * @return estimated accelerometer bias or null if not available.
655      */
656     @Override
657     public AccelerationTriad getEstimatedBiasAsTriad() {
658         return estimatedBiases != null
659                 ? new AccelerationTriad(AccelerationUnit.METERS_PER_SQUARED_SECOND,
660                 estimatedBiases[0], estimatedBiases[1], estimatedBiases[2])
661                 : null;
662     }
663 
664     /**
665      * Gets estimated accelerometer bias.
666      *
667      * @param result instance where result will be stored.
668      * @return true if estimated accelerometer bias is available and result was
669      * modified, false otherwise.
670      */
671     @Override
672     public boolean getEstimatedBiasAsTriad(final AccelerationTriad result) {
673         if (estimatedBiases != null) {
674             result.setValueCoordinatesAndUnit(
675                     estimatedBiases[0], estimatedBiases[1], estimatedBiases[2],
676                     AccelerationUnit.METERS_PER_SQUARED_SECOND);
677             return true;
678         } else {
679             return false;
680         }
681     }
682 
683     /**
684      * Gets estimated accelerometer scale factors and cross coupling errors.
685      * This is the product of matrix Ta containing cross coupling errors and Ka
686      * containing scaling factors.
687      * So tat:
688      * <pre>
689      *     Ma = [sx    mxy  mxz] = Ta*Ka
690      *          [myx   sy   myz]
691      *          [mzx   mzy  sz ]
692      * </pre>
693      * Where:
694      * <pre>
695      *     Ka = [sx 0   0 ]
696      *          [0  sy  0 ]
697      *          [0  0   sz]
698      * </pre>
699      * and
700      * <pre>
701      *     Ta = [1          -alphaXy    alphaXz ]
702      *          [alphaYx    1           -alphaYz]
703      *          [-alphaZx   alphaZy     1       ]
704      * </pre>
705      * Hence:
706      * <pre>
707      *     Ma = [sx    mxy  mxz] = Ta*Ka =  [sx             -sy * alphaXy   sz * alphaXz ]
708      *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
709      *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
710      * </pre>
711      * This instance allows any 3x3 matrix however, typically alphaYx, alphaZx and alphaZy
712      * are considered to be zero if the accelerometer z-axis is assumed to be the same
713      * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Ma matrix
714      * becomes upper diagonal:
715      * <pre>
716      *     Ma = [sx    mxy  mxz]
717      *          [0     sy   myz]
718      *          [0     0    sz ]
719      * </pre>
720      * Values of this matrix are unit-less.
721      *
722      * @return estimated accelerometer scale factors and cross coupling errors, or null
723      * if not available.
724      */
725     @Override
726     public Matrix getEstimatedMa() {
727         return estimatedMa;
728     }
729 
730     /**
731      * Gets estimated x-axis scale factor.
732      *
733      * @return estimated x-axis scale factor or null if not available.
734      */
735     @Override
736     public Double getEstimatedSx() {
737         return estimatedMa != null ? estimatedMa.getElementAt(0, 0) : null;
738     }
739 
740     /**
741      * Gets estimated y-axis scale factor.
742      *
743      * @return estimated y-axis scale factor or null if not available.
744      */
745     @Override
746     public Double getEstimatedSy() {
747         return estimatedMa != null ? estimatedMa.getElementAt(1, 1) : null;
748     }
749 
750     /**
751      * Gets estimated z-axis scale factor.
752      *
753      * @return estimated z-axis scale factor or null if not available.
754      */
755     @Override
756     public Double getEstimatedSz() {
757         return estimatedMa != null ? estimatedMa.getElementAt(2, 2) : null;
758     }
759 
760     /**
761      * Gets estimated x-y cross-coupling error.
762      *
763      * @return estimated x-y cross-coupling error or null if not available.
764      */
765     @Override
766     public Double getEstimatedMxy() {
767         return estimatedMa != null ? estimatedMa.getElementAt(0, 1) : null;
768     }
769 
770     /**
771      * Gets estimated x-z cross-coupling error.
772      *
773      * @return estimated x-z cross-coupling error or null if not available.
774      */
775     @Override
776     public Double getEstimatedMxz() {
777         return estimatedMa != null ? estimatedMa.getElementAt(0, 2) : null;
778     }
779 
780     /**
781      * Gets estimated y-x cross-coupling error.
782      *
783      * @return estimated y-x cross-coupling error or null if not available.
784      */
785     @Override
786     public Double getEstimatedMyx() {
787         return estimatedMa != null ? estimatedMa.getElementAt(1, 0) : null;
788     }
789 
790     /**
791      * Gets estimated y-z cross-coupling error.
792      *
793      * @return estimated y-z cross-coupling error or null if not available.
794      */
795     @Override
796     public Double getEstimatedMyz() {
797         return estimatedMa != null ? estimatedMa.getElementAt(1, 2) : null;
798     }
799 
800     /**
801      * Gets estimated z-x cross-coupling error.
802      *
803      * @return estimated z-x cross-coupling error or null if not available.
804      */
805     @Override
806     public Double getEstimatedMzx() {
807         return estimatedMa != null ? estimatedMa.getElementAt(2, 0) : null;
808     }
809 
810     /**
811      * Gets estimated z-y cross-coupling error.
812      *
813      * @return estimated z-y cross-coupling error or null if not available.
814      */
815     @Override
816     public Double getEstimatedMzy() {
817         return estimatedMa != null ? estimatedMa.getElementAt(2, 1) : null;
818     }
819 
820     /**
821      * Internal method to perform calibration when common z-axis is assumed for both
822      * the accelerometer and gyroscope.
823      *
824      * @throws AlgebraException if there are numerical errors.
825      */
826     private void calibrateCommonAxis() throws AlgebraException {
827         // The accelerometer model is:
828         // fmeas = ba + (I + Ma) * ftrue + w
829 
830         // Ideally a least squares solution tries to minimize noise component, so:
831         // fmeas = ba + (I + Ma) * ftrue
832 
833         // Hence:
834         // [fmeasx] = [bx] + ( [1  0   0] + [sx    mxy mxz])   [ftruex]
835         // [fmeasy] = [by]     [0  1   0]   [myx   sy  myz]    [ftruey]
836         // [fmeasz] = [bz]     [0  0   1]   [mzx   mzy sz ]    [ftruez]
837 
838         // where myx = mzx = mzy = 0
839 
840         // Hence:
841         // [fmeasx] = [bx] + ( [1  0   0] + [sx    mxy mxz])   [ftruex]
842         // [fmeasy] = [by]     [0  1   0]   [0     sy  myz]    [ftruey]
843         // [fmeasz] = [bz]     [0  0   1]   [0     0   sz ]    [ftruez]
844 
845         // [fmeasx] = [bx] +   [1+sx   mxy     mxz ][ftruex]
846         // [fmeasy]   [by]     [0      1+sy    myz ][ftruey]
847         // [fmeasz]   [bz]     [0      0       1+sz][ftruez]
848 
849         // fmeasx = bx + (1+sx) * ftruex + mxy * ftruey + mxz * ftruez
850         // fmeasy = by + (1+sy) * ftruey + myz * ftruez
851         // fmeasz = bz + (1+sz) * ftruez
852 
853         // Where the unknowns are: bx, by, bz, sx, sy, sz, mxy mxz, myz
854         // Reordering:
855         // fmeasx = bx + ftruex + sx * ftruex + mxy * ftruey + mxz * ftruez
856         // fmeasy = by + ftruey + sy * ftruey + myz * ftruez
857         // fmeasz = bz + ftruez + sz * ftruez
858 
859         // fmeasx - ftruex = bx + sx * ftruex + mxy * ftruey + mxz * ftruez
860         // fmeasy - ftruey = by + sy * ftruey + myz * ftruez
861         // fmeasz - ftruez = bz + sz * ftruez
862 
863         // [1   0   0   ftruex  0       0       ftruey  ftruez  0     ][bx ] = [fmeasx - ftruex]
864         // [0   1   0   0       ftruey  0       0       0       ftruez][by ]   [fmeasy - ftruey]
865         // [0   0   1   0       0       ftruez  0       0       0     ][bz ]   [fmeasz - ftruez]
866         //                                                             [sx ]
867         //                                                             [sy ]
868         //                                                             [sz ]
869         //                                                             [mxy]
870         //                                                             [mxz]
871         //                                                             [myz]
872 
873         final var expectedKinematics = new BodyKinematics();
874 
875         final var rows = EQUATIONS_PER_MEASUREMENT * measurements.size();
876         final var a = new Matrix(rows, COMMON_Z_AXIS_UNKNOWNS);
877         final var b = new Matrix(rows, 1);
878         var i = 0;
879         for (final var measurement : measurements) {
880             final var measuredKinematics = measurement.getKinematics();
881             final var ecefFrame = measurement.getFrame();
882             final var previousEcefFrame = measurement.getPreviousFrame();
883             final var timeInterval = measurement.getTimeInterval();
884 
885             ECEFKinematicsEstimator.estimateKinematics(timeInterval, ecefFrame, previousEcefFrame, expectedKinematics);
886 
887             final var fMeasX = measuredKinematics.getFx();
888             final var fMeasY = measuredKinematics.getFy();
889             final var fMeasZ = measuredKinematics.getFz();
890 
891             final var fTrueX = expectedKinematics.getFx();
892             final var fTrueY = expectedKinematics.getFy();
893             final var fTrueZ = expectedKinematics.getFz();
894 
895             a.setElementAt(i, 0, 1.0);
896             a.setElementAt(i, 3, fTrueX);
897             a.setElementAt(i, 6, fTrueY);
898             a.setElementAt(i, 7, fTrueZ);
899 
900             b.setElementAtIndex(i, fMeasX - fTrueX);
901             i++;
902 
903             a.setElementAt(i, 1, 1.0);
904             a.setElementAt(i, 4, fTrueY);
905             a.setElementAt(i, 8, fTrueZ);
906 
907             b.setElementAtIndex(i, fMeasY - fTrueY);
908             i++;
909 
910             a.setElementAt(i, 2, 1.0);
911             a.setElementAt(i, 5, fTrueZ);
912 
913             b.setElementAtIndex(i, fMeasZ - fTrueZ);
914             i++;
915         }
916 
917         final var unknowns = Utils.solve(a, b);
918 
919         final var bx = unknowns.getElementAtIndex(0);
920         final var by = unknowns.getElementAtIndex(1);
921         final var bz = unknowns.getElementAtIndex(2);
922         final var sx = unknowns.getElementAtIndex(3);
923         final var sy = unknowns.getElementAtIndex(4);
924         final var sz = unknowns.getElementAtIndex(5);
925         final var mxy = unknowns.getElementAtIndex(6);
926         final var mxz = unknowns.getElementAtIndex(7);
927         final var myz = unknowns.getElementAtIndex(8);
928 
929         fillBiases(bx, by, bz);
930         fillMa(sx, sy, sz, mxy, mxz, 0.0, myz, 0.0, 0.0);
931     }
932 
933     /**
934      * Internal method to perform general calibration.
935      *
936      * @throws AlgebraException if there are numerical errors.
937      */
938     private void calibrateGeneral() throws AlgebraException {
939         // The accelerometer model is:
940         // fmeas = ba + (I + Ma) * ftrue + w
941 
942         // Ideally a least squares solution tries to minimize noise component, so:
943         // fmeas = ba + (I + Ma) * ftrue
944 
945         // Hence:
946         // [fmeasx] = [bx] + ( [1  0   0] + [sx    mxy mxz])   [ftruex]
947         // [fmeasy]   [by]     [0  1   0]   [myx   sy  myz]    [ftruey]
948         // [fmeasz]   [bz]     [0  0   1]   [mzx   mzy sz ]    [ftruez]
949 
950         // [fmeasx] = [bx] +   [1+sx   mxy     mxz ][ftruex]
951         // [fmeasy]   [by]     [myx    1+sy    myz ][ftruey]
952         // [fmeasz]   [bz]     [mzx    mzy     1+sz][ftruez]
953 
954         // fmeasx = bx + (1+sx) * ftruex + mxy * ftruey + mxz * ftruez
955         // fmeasy = by + myx * ftruex + (1+sy) * ftruey + myz * ftruez
956         // fmeasz = bz + mzx * ftruex + mzy * ftruey + (1+sz) * ftruez
957 
958         // Where the unknowns are: bx, by, bz, sx, sy, sz, mxy mxz, myx, myz, mzx, mzy
959         // Reordering:
960         // fmeasx = bx + ftruex + sx * ftruex + mxy * ftruey + mxz * ftruez
961         // fmeasy = by + myx * ftruex + ftruey + sy * ftruey + myz * ftruez
962         // fmeasz = bz + mzx * ftruex + mzy * ftruey + ftruez + sz * ftruez
963 
964         // fmeasx - ftruex = bx + sx * ftruex + mxy * ftruey + mxz * ftruez
965         // fmeasy - ftruey = by + myx * ftruex + sy * ftruey + myz * ftruez
966         // fmeasz - ftruez = bz + mzx * ftruex + mzy * ftruey + sz * ftruez
967 
968         // [1   0   0   ftruex  0       0       ftruey  ftruez  0       0       0       0     ][bx ] = [fmeasx - ftruex]
969         // [0   1   0   0       ftruey  0       0       0       ftruex  ftruez  0       0     ][by ]   [fmeasy - ftruey]
970         // [0   0   1   0       0       ftruez  0       0       0       0       ftruex  ftruey][bz ]   [fmeasz - ftruez]
971         //                                                                                     [sx ]
972         //                                                                                     [sy ]
973         //                                                                                     [sz ]
974         //                                                                                     [mxy]
975         //                                                                                     [mxz]
976         //                                                                                     [myx]
977         //                                                                                     [myz]
978         //                                                                                     [mzx]
979         //                                                                                     [mzy]
980 
981         final var expectedKinematics = new BodyKinematics();
982 
983         final var rows = EQUATIONS_PER_MEASUREMENT * measurements.size();
984         final var a = new Matrix(rows, GENERAL_UNKNOWNS);
985         final var b = new Matrix(rows, 1);
986         var i = 0;
987         for (final var measurement : measurements) {
988             final var measuredKinematics = measurement.getKinematics();
989             final var ecefFrame = measurement.getFrame();
990             final var previousEcefFrame = measurement.getPreviousFrame();
991             final var timeInterval = measurement.getTimeInterval();
992 
993             ECEFKinematicsEstimator.estimateKinematics(timeInterval, ecefFrame, previousEcefFrame, expectedKinematics);
994 
995             final var fMeasX = measuredKinematics.getFx();
996             final var fMeasY = measuredKinematics.getFy();
997             final var fMeasZ = measuredKinematics.getFz();
998 
999             final var fTrueX = expectedKinematics.getFx();
1000             final var fTrueY = expectedKinematics.getFy();
1001             final var fTrueZ = expectedKinematics.getFz();
1002 
1003             a.setElementAt(i, 0, 1.0);
1004             a.setElementAt(i, 3, fTrueX);
1005             a.setElementAt(i, 6, fTrueY);
1006             a.setElementAt(i, 7, fTrueZ);
1007 
1008             b.setElementAtIndex(i, fMeasX - fTrueX);
1009             i++;
1010 
1011             a.setElementAt(i, 1, 1.0);
1012             a.setElementAt(i, 4, fTrueY);
1013             a.setElementAt(i, 8, fTrueX);
1014             a.setElementAt(i, 9, fTrueZ);
1015 
1016             b.setElementAtIndex(i, fMeasY - fTrueY);
1017             i++;
1018 
1019             a.setElementAt(i, 2, 1.0);
1020             a.setElementAt(i, 5, fTrueZ);
1021             a.setElementAt(i, 10, fTrueX);
1022             a.setElementAt(i, 11, fTrueY);
1023 
1024             b.setElementAtIndex(i, fMeasZ - fTrueZ);
1025             i++;
1026         }
1027 
1028         final var unknowns = Utils.solve(a, b);
1029 
1030         final var bx = unknowns.getElementAtIndex(0);
1031         final var by = unknowns.getElementAtIndex(1);
1032         final var bz = unknowns.getElementAtIndex(2);
1033         final var sx = unknowns.getElementAtIndex(3);
1034         final var sy = unknowns.getElementAtIndex(4);
1035         final var sz = unknowns.getElementAtIndex(5);
1036         final var mxy = unknowns.getElementAtIndex(6);
1037         final var mxz = unknowns.getElementAtIndex(7);
1038         final var myx = unknowns.getElementAtIndex(8);
1039         final var myz = unknowns.getElementAtIndex(9);
1040         final var mzx = unknowns.getElementAtIndex(10);
1041         final var mzy = unknowns.getElementAtIndex(11);
1042 
1043         fillBiases(bx, by, bz);
1044         fillMa(sx, sy, sz, mxy, mxz, myx, myz, mzx, mzy);
1045     }
1046 
1047     /**
1048      * Fills estimated biases array with estimated values.
1049      *
1050      * @param bx x coordinate of bias.
1051      * @param by y coordinate of bias.
1052      * @param bz z coordinate of bias.
1053      */
1054     private void fillBiases(final double bx, final double by, final double bz) {
1055         if (estimatedBiases == null) {
1056             estimatedBiases = new double[BodyKinematics.COMPONENTS];
1057         }
1058 
1059         estimatedBiases[0] = bx;
1060         estimatedBiases[1] = by;
1061         estimatedBiases[2] = bz;
1062     }
1063 
1064     /**
1065      * Fills scale factor and cross coupling error matrix with estimated values.
1066      *
1067      * @param sx  x scale factor
1068      * @param sy  y scale factor
1069      * @param sz  z scale factor
1070      * @param mxy x-y cross coupling
1071      * @param mxz x-z cross coupling
1072      * @param myx y-x cross coupling
1073      * @param myz y-z cross coupling
1074      * @param mzx z-x cross coupling
1075      * @param mzy z-y cross coupling
1076      * @throws WrongSizeException never happens.
1077      */
1078     private void fillMa(final double sx, final double sy, final double sz,
1079                         final double mxy, final double mxz, final double myx,
1080                         final double myz, final double mzx, final double mzy) throws WrongSizeException {
1081         if (estimatedMa == null) {
1082             estimatedMa = new Matrix(BodyKinematics.COMPONENTS, BodyKinematics.COMPONENTS);
1083         }
1084 
1085         estimatedMa.setElementAt(0, 0, sx);
1086         estimatedMa.setElementAt(1, 0, myx);
1087         estimatedMa.setElementAt(2, 0, mzx);
1088 
1089         estimatedMa.setElementAt(0, 1, mxy);
1090         estimatedMa.setElementAt(1, 1, sy);
1091         estimatedMa.setElementAt(2, 1, mzy);
1092 
1093         estimatedMa.setElementAt(0, 2, mxz);
1094         estimatedMa.setElementAt(1, 2, myz);
1095         estimatedMa.setElementAt(2, 2, sz);
1096     }
1097 }