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 }