1 /*
2 * Copyright (C) 2021 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.intervals.thresholdfactor;
17
18 import com.irurueta.algebra.Matrix;
19 import com.irurueta.navigation.LockedException;
20 import com.irurueta.navigation.NotReadyException;
21 import com.irurueta.navigation.inertial.BodyKinematicsAndMagneticFluxDensity;
22 import com.irurueta.navigation.inertial.calibration.AccelerometerNoiseRootPsdSource;
23 import com.irurueta.navigation.inertial.calibration.CalibrationException;
24 import com.irurueta.navigation.inertial.calibration.StandardDeviationBodyMagneticFluxDensity;
25 import com.irurueta.navigation.inertial.calibration.generators.MagnetometerMeasurementsGenerator;
26 import com.irurueta.navigation.inertial.calibration.generators.MagnetometerMeasurementsGeneratorListener;
27 import com.irurueta.navigation.inertial.calibration.intervals.TriadStaticIntervalDetector;
28 import com.irurueta.navigation.inertial.calibration.magnetometer.KnownHardIronMagnetometerCalibrator;
29 import com.irurueta.navigation.inertial.calibration.magnetometer.MagnetometerCalibratorMeasurementType;
30 import com.irurueta.navigation.inertial.calibration.magnetometer.MagnetometerNonLinearCalibrator;
31 import com.irurueta.navigation.inertial.calibration.magnetometer.OrderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator;
32 import com.irurueta.navigation.inertial.calibration.magnetometer.QualityScoredMagnetometerCalibrator;
33 import com.irurueta.navigation.inertial.calibration.magnetometer.UnknownHardIronMagnetometerCalibrator;
34 import com.irurueta.navigation.inertial.calibration.magnetometer.UnorderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator;
35 import com.irurueta.units.Acceleration;
36 import com.irurueta.units.AccelerationUnit;
37 import com.irurueta.units.Time;
38
39 import java.util.ArrayList;
40 import java.util.List;
41
42 /**
43 * Optimizes the threshold factor for interval detection of magnetometer data based
44 * on results of calibration.
45 * Implementations of this class will attempt to find the best threshold factor
46 * between the provided range of values.
47 * Only magnetometer calibrators based on unknown orientation are supported, in other terms,
48 * calibrators must be {@link MagnetometerNonLinearCalibrator} and must support
49 * {@link MagnetometerCalibratorMeasurementType#STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY}.
50 */
51 public abstract class MagnetometerIntervalDetectorThresholdFactorOptimizer extends
52 IntervalDetectorThresholdFactorOptimizer<BodyKinematicsAndMagneticFluxDensity,
53 MagnetometerIntervalDetectorThresholdFactorOptimizerDataSource> implements
54 AccelerometerNoiseRootPsdSource {
55
56 /**
57 * Default minimum threshold factor.
58 */
59 public static final double DEFAULT_MIN_THRESHOLD_FACTOR = 2.0;
60
61 /**
62 * Default maximum threshold factor.
63 */
64 public static final double DEFAULT_MAX_THRESHOLD_FACTOR = 10.0;
65
66 /**
67 * Minimum threshold factor.
68 */
69 protected double minThresholdFactor = DEFAULT_MIN_THRESHOLD_FACTOR;
70
71 /**
72 * Maximum threshold factor.
73 */
74 protected double maxThresholdFactor = DEFAULT_MAX_THRESHOLD_FACTOR;
75
76 /**
77 * Magnetometer calibrator.
78 */
79 private MagnetometerNonLinearCalibrator calibrator;
80
81 /**
82 * A measurement generator for magnetometer calibrators.
83 */
84 private MagnetometerMeasurementsGenerator generator;
85
86 /**
87 * Generated measurements to be used for magnetometer calibration.
88 */
89 private List<StandardDeviationBodyMagneticFluxDensity> measurements;
90
91 /**
92 * Mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} measurements
93 * into quality scores.
94 */
95 private QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> qualityScoreMapper =
96 new DefaultMagnetometerQualityScoreMapper();
97
98 /**
99 * Accelerometer base noise level that has been detected during
100 * initialization of the best solution that has been found expressed in
101 * meters per squared second (m/s^2).
102 * This is equal to the standard deviation of the accelerometer measurements
103 * during the initialization phase.
104 */
105 private double baseNoiseLevel;
106
107 /**
108 * Threshold to determine static/dynamic period changes expressed in
109 * meters per squared second (m/s^2) for the best calibration solution that
110 * has been found.
111 */
112 private double threshold;
113
114 /**
115 * Estimated covariance matrix for estimated parameters.
116 */
117 private Matrix estimatedCovariance;
118
119 /**
120 * Estimated magnetometer soft-iron matrix containing scale factors
121 * and cross-coupling errors.
122 * This is the product of matrix Tm containing cross-coupling errors and Km
123 * containing scaling factors.
124 * So that:
125 * <pre>
126 * Mm = [sx mxy mxz] = Tm*Km
127 * [myx sy myz]
128 * [mzx mzy sz ]
129 * </pre>
130 * Where:
131 * <pre>
132 * Km = [sx 0 0 ]
133 * [0 sy 0 ]
134 * [0 0 sz]
135 * </pre>
136 * and
137 * <pre>
138 * Tm = [1 -alphaXy alphaXz ]
139 * [alphaYx 1 -alphaYz]
140 * [-alphaZx alphaZy 1 ]
141 * </pre>
142 * Hence:
143 * <pre>
144 * Mm = [sx mxy mxz] = Tm*Km = [sx -sy * alphaXy sz * alphaXz ]
145 * [myx sy myz] [sx * alphaYx sy -sz * alphaYz]
146 * [mzx mzy sz ] [-sx * alphaZx sy * alphaZy sz ]
147 * </pre>
148 * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
149 * are considered to be zero if the accelerometer z-axis is assumed to be the same
150 * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mm matrix
151 * becomes upper diagonal:
152 * <pre>
153 * Mm = [sx mxy mxz]
154 * [0 sy myz]
155 * [0 0 sz ]
156 * </pre>
157 * Values of this matrix are unit-less.
158 */
159 private Matrix estimatedMm;
160
161 /**
162 * Estimated magnetometer hard-iron biases for each magnetometer axis
163 * expressed in Teslas (T).
164 */
165 private double[] estimatedHardIron;
166
167 /**
168 * Constructor.
169 */
170 protected MagnetometerIntervalDetectorThresholdFactorOptimizer() {
171 initialize();
172 }
173
174 /**
175 * Constructor.
176 *
177 * @param dataSource instance in charge of retrieving data for this optimizer.
178 */
179 protected MagnetometerIntervalDetectorThresholdFactorOptimizer(
180 final MagnetometerIntervalDetectorThresholdFactorOptimizerDataSource dataSource) {
181 super(dataSource);
182 initialize();
183 }
184
185 /**
186 * Constructor.
187 *
188 * @param calibrator a magnetometer calibrator to be used to optimize its
189 * Mean Square Error (MSE).
190 * @throws IllegalArgumentException if magnetometer calibrator does not use
191 * {@link StandardDeviationBodyMagneticFluxDensity} measurements.
192 */
193 protected MagnetometerIntervalDetectorThresholdFactorOptimizer(final MagnetometerNonLinearCalibrator calibrator) {
194 try {
195 setCalibrator(calibrator);
196 } catch (final LockedException ignore) {
197 // never happens
198 }
199 initialize();
200 }
201
202 /**
203 * Constructor.
204 *
205 * @param dataSource instance in charge of retrieving data for this optimizer.
206 * @param calibrator a magnetometer calibrator to be used to optimize its
207 * Mean Square Error (MSE).
208 * @throws IllegalArgumentException if magnetometer calibrator does not use
209 * {@link StandardDeviationBodyMagneticFluxDensity} measurements.
210 */
211 protected MagnetometerIntervalDetectorThresholdFactorOptimizer(
212 final MagnetometerIntervalDetectorThresholdFactorOptimizerDataSource dataSource,
213 final MagnetometerNonLinearCalibrator calibrator) {
214 super(dataSource);
215 try {
216 setCalibrator(calibrator);
217 } catch (final LockedException ignore) {
218 // never happens
219 }
220 initialize();
221 }
222
223 /**
224 * Gets provided magnetometer calibrator to be used to optimize its Mean Square Error (MSE).
225 *
226 * @return magnetometer calibrator to be used to optimize its MSE.
227 */
228 public MagnetometerNonLinearCalibrator getCalibrator() {
229 return calibrator;
230 }
231
232 /**
233 * Sets a magnetometer calibrator to be used to optimize its Mean Square Error.
234 *
235 * @param calibrator magnetometer calibrator to be used to optimize its MSE.
236 * @throws LockedException if optimizer is already running.
237 * @throws IllegalArgumentException if magnetometer calibrator does not use
238 * {@link StandardDeviationBodyMagneticFluxDensity} measurements.
239 */
240 public void setCalibrator(final MagnetometerNonLinearCalibrator calibrator) throws LockedException {
241 if (running) {
242 throw new LockedException();
243 }
244
245 if (calibrator != null && calibrator.getMeasurementType()
246 != MagnetometerCalibratorMeasurementType.STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY) {
247 throw new IllegalArgumentException();
248 }
249
250 this.calibrator = calibrator;
251 }
252
253 /**
254 * Gets mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} measurements into
255 * quality scores.
256 *
257 * @return mapper to convert measurements into quality scores.
258 */
259 public QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> getQualityScoreMapper() {
260 return qualityScoreMapper;
261 }
262
263 /**
264 * Sets mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} measurements into
265 * quality scores.
266 *
267 * @param qualityScoreMapper mapper to convert measurements into quality scores.
268 * @throws LockedException if optimizer is already running.
269 */
270 public void setQualityScoreMapper(
271 final QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> qualityScoreMapper)
272 throws LockedException {
273 if (running) {
274 throw new LockedException();
275 }
276
277 this.qualityScoreMapper = qualityScoreMapper;
278 }
279
280 /**
281 * Gets the minimum threshold factor.
282 *
283 * @return minimum threshold factor.
284 */
285 public double getMinThresholdFactor() {
286 return minThresholdFactor;
287 }
288
289 /**
290 * Gets the maximum threshold factor.
291 *
292 * @return maximum threshold factor.
293 */
294 public double getMaxThresholdFactor() {
295 return maxThresholdFactor;
296 }
297
298 /**
299 * Sets a range of threshold factor values to get an optimized
300 * threshold factor value.
301 *
302 * @param minThresholdFactor minimum threshold.
303 * @param maxThresholdFactor maximum threshold.
304 * @throws LockedException if optimizer is already running.
305 * @throws IllegalArgumentException if either minimum or maximum values are
306 * negative, or if the minimum value is larger
307 * than the maximum one.
308 */
309 public void setThresholdFactorRange(final double minThresholdFactor, final double maxThresholdFactor)
310 throws LockedException {
311 if (running) {
312 throw new LockedException();
313 }
314 if (minThresholdFactor < 0.0 || maxThresholdFactor < 0.0 || minThresholdFactor >= maxThresholdFactor) {
315 throw new IllegalArgumentException();
316 }
317
318 this.minThresholdFactor = minThresholdFactor;
319 this.maxThresholdFactor = maxThresholdFactor;
320 }
321
322 /**
323 * Indicates whether this optimizer is ready to start optimization.
324 *
325 * @return true if this optimizer is ready, false otherwise.
326 */
327 @Override
328 public boolean isReady() {
329 return super.isReady() && calibrator != null && qualityScoreMapper != null;
330 }
331
332 /**
333 * Gets the time interval between input measurements provided to the
334 * {@link #getDataSource()} expressed in seconds (s).
335 *
336 * @return time interval between input measurements.
337 */
338 public double getTimeInterval() {
339 return generator.getTimeInterval();
340 }
341
342 /**
343 * Sets time interval between input measurements provided to the
344 * {@link #getDataSource()} expressed in seconds (s).
345 *
346 * @param timeInterval time interval between input measurements.
347 * @throws LockedException if optimizer is already running.
348 * @throws IllegalArgumentException if provided value is negative.
349 */
350 public void setTimeInterval(final double timeInterval) throws LockedException {
351 if (running) {
352 throw new LockedException();
353 }
354
355 generator.setTimeInterval(timeInterval);
356 }
357
358 /**
359 * Gets the time interval between input measurements provided to the
360 * {@link #getDataSource()}.
361 *
362 * @return time interval between input measurements.
363 */
364 public Time getTimeIntervalAsTime() {
365 return generator.getTimeIntervalAsTime();
366 }
367
368 /**
369 * Gets the time interval between input measurements provided to the
370 * {@link #getDataSource()}.
371 *
372 * @param result instance where the time interval will be stored.
373 */
374 public void getTimeIntervalAsTime(final Time result) {
375 generator.getTimeIntervalAsTime(result);
376 }
377
378 /**
379 * Sets time interval between input measurements provided to the
380 * {@link #getDataSource()}.
381 *
382 * @param timeInterval time interval between input measurements.
383 * @throws LockedException if optimizer is already running.
384 * @throws IllegalArgumentException if provided value is negative.
385 */
386 public void setTimeInterval(final Time timeInterval) throws LockedException {
387 if (running) {
388 throw new LockedException();
389 }
390
391 generator.setTimeInterval(timeInterval);
392 }
393
394 /**
395 * Gets minimum number of input measurements provided to the
396 * {@link #getDataSource()} required in a static interval to be taken
397 * into account.
398 * Smaller static intervals will be discarded.
399 *
400 * @return a minimum number of input measurements required in a static interval
401 * to be taken into account.
402 */
403 public int getMinStaticSamples() {
404 return generator.getMinStaticSamples();
405 }
406
407 /**
408 * Sets minimum number of input measurements provided to the
409 * {@link #getDataSource()} required in a static interval to be taken
410 * into account.
411 * Smaller static intervals will be discarded.
412 *
413 * @param minStaticSamples a minimum number of input measurements required in
414 * a static interval to be taken into account.
415 * @throws LockedException if optimizer is already running.
416 * @throws IllegalArgumentException if provided value is less than 2.
417 */
418 public void setMinStaticSamples(final int minStaticSamples)
419 throws LockedException {
420 if (running) {
421 throw new LockedException();
422 }
423
424 generator.setMinStaticSamples(minStaticSamples);
425 }
426
427 /**
428 * Gets maximum number of input measurements provided to the
429 * {@link #getDataSource()} allowed in dynamic intervals.
430 *
431 * @return maximum number of input measurements allowed in dynamic intervals.
432 */
433 public int getMaxDynamicSamples() {
434 return generator.getMaxDynamicSamples();
435 }
436
437 /**
438 * Sets maximum number of input measurements provided to the
439 * {@link #getDataSource()} allowed in dynamic intervals.
440 *
441 * @param maxDynamicSamples maximum number of input measurements allowed in
442 * dynamic intervals.
443 * @throws LockedException if optimizer is already running.
444 * @throws IllegalArgumentException if provided value is less than 2.
445 */
446 public void setMaxDynamicSamples(final int maxDynamicSamples) throws LockedException {
447 if (running) {
448 throw new LockedException();
449 }
450
451 generator.setMaxDynamicSamples(maxDynamicSamples);
452 }
453
454 /**
455 * Gets length of number of input measurements provided to the
456 * {@link #getDataSource()} to keep within the window being processed
457 * to determine instantaneous accelerometer noise level.
458 *
459 * @return length of input measurements to keep within the window.
460 */
461 public int getWindowSize() {
462 return generator.getWindowSize();
463 }
464
465 /**
466 * Sets length of number of input measurements provided to the
467 * {@link #getDataSource()} to keep within the window being processed
468 * to determine instantaneous noise level.
469 * Window size must always be larger than the allowed minimum value, which is 2
470 * and must have an odd value.
471 *
472 * @param windowSize length of number of samples to keep within the window.
473 * @throws LockedException if optimizer is already running.
474 * @throws IllegalArgumentException if provided value is not valid.
475 */
476 public void setWindowSize(final int windowSize) throws LockedException {
477 if (running) {
478 throw new LockedException();
479 }
480
481 generator.setWindowSize(windowSize);
482 }
483
484 /**
485 * Gets number of input measurements provided to the
486 * {@link #getDataSource()} to be processed initially while keeping the sensor
487 * static to find the base noise level when the device is static.
488 *
489 * @return number of samples to be processed initially.
490 */
491 public int getInitialStaticSamples() {
492 return generator.getInitialStaticSamples();
493 }
494
495 /**
496 * Sets number of input parameters provided to the {@link #getDataSource()}
497 * to be processed initially while keeping the sensor static to
498 * find the base noise level when the device is static.
499 *
500 * @param initialStaticSamples number of samples ot be processed initially.
501 * @throws LockedException if optimizer is already running.
502 * @throws IllegalArgumentException if provided value is less than
503 * {@link TriadStaticIntervalDetector#MINIMUM_INITIAL_STATIC_SAMPLES}.
504 */
505 public void setInitialStaticSamples(final int initialStaticSamples) throws LockedException {
506 if (running) {
507 throw new LockedException();
508 }
509
510 generator.setInitialStaticSamples(initialStaticSamples);
511 }
512
513 /**
514 * Gets factor to determine that a sudden movement has occurred during
515 * initialization if instantaneous noise level exceeds accumulated noise
516 * level by this factor amount.
517 * This factor is unit-less.
518 *
519 * @return factor to determine that a sudden movement has occurred.
520 */
521 public double getInstantaneousNoiseLevelFactor() {
522 return generator.getInstantaneousNoiseLevelFactor();
523 }
524
525 /**
526 * Sets factor to determine that a sudden movement has occurred during
527 * initialization if instantaneous noise level exceeds accumulated noise
528 * level by this factor amount.
529 * This factor is unit-less.
530 *
531 * @param instantaneousNoiseLevelFactor factor to determine that a sudden
532 * movement has occurred during
533 * initialization.
534 * @throws LockedException if optimizer is already running.
535 * @throws IllegalArgumentException if provided value is zero or negative.
536 */
537 public void setInstantaneousNoiseLevelFactor(final double instantaneousNoiseLevelFactor) throws LockedException {
538 if (running) {
539 throw new LockedException();
540 }
541
542 generator.setInstantaneousNoiseLevelFactor(instantaneousNoiseLevelFactor);
543 }
544
545 /**
546 * Gets the overall absolute threshold to determine whether there has been
547 * excessive motion during the whole initialization phase.
548 * This threshold is expressed in meters per squared second (m/s^2).
549 *
550 * @return overall absolute threshold to determine whether there has been
551 * excessive motion.
552 */
553 public double getBaseNoiseLevelAbsoluteThreshold() {
554 return generator.getBaseNoiseLevelAbsoluteThreshold();
555 }
556
557 /**
558 * Sets the overall absolute threshold to determine whether there has been
559 * excessive motion during the whole initialization phase.
560 * This threshold is expressed in meters per squared second (m/s^2).
561 *
562 * @param baseNoiseLevelAbsoluteThreshold overall absolute threshold to
563 * determine whether there has been
564 * excessive motion.
565 * @throws LockedException if optimizer is already running.
566 * @throws IllegalArgumentException if provided value is zero or negative.
567 */
568 public void setBaseNoiseLevelAbsoluteThreshold(final double baseNoiseLevelAbsoluteThreshold)
569 throws LockedException {
570 if (running) {
571 throw new LockedException();
572 }
573
574 generator.setBaseNoiseLevelAbsoluteThreshold(baseNoiseLevelAbsoluteThreshold);
575 }
576
577 /**
578 * Gets the overall absolute threshold to determine whether there has been
579 * excessive motion during the whole initialization phase.
580 *
581 * @return overall absolute threshold to determine whether there has been
582 * excessive motion.
583 */
584 public Acceleration getBaseNoiseLevelAbsoluteThresholdAsMeasurement() {
585 return generator.getBaseNoiseLevelAbsoluteThresholdAsMeasurement();
586 }
587
588 /**
589 * Gets the overall absolute threshold to determine whether there has been
590 * excessive motion during the whole initialization phase.
591 *
592 * @param result instance where the result will be stored.
593 */
594 public void getBaseNoiseLevelAbsoluteThresholdAsMeasurement(final Acceleration result) {
595 generator.getBaseNoiseLevelAbsoluteThresholdAsMeasurement(result);
596 }
597
598 /**
599 * Sets the overall absolute threshold to determine whether there has been
600 * excessive motion during the whole initialization phase.
601 *
602 * @param baseNoiseLevelAbsoluteThreshold overall absolute threshold to
603 * determine whether there has been
604 * excessive motion.
605 * @throws LockedException if optimizer is already running.
606 * @throws IllegalArgumentException if provided value is zero or negative.
607 */
608 public void setBaseNoiseLevelAbsoluteThreshold(final Acceleration baseNoiseLevelAbsoluteThreshold)
609 throws LockedException {
610 if (running) {
611 throw new LockedException();
612 }
613
614 generator.setBaseNoiseLevelAbsoluteThreshold(baseNoiseLevelAbsoluteThreshold);
615 }
616
617 /**
618 * Gets accelerometer base noise level that has been detected during
619 * initialization of the best solution that has been found expressed in
620 * meters per squared second (m/s^2).
621 * This is equal to the standard deviation of the accelerometer measurements
622 * during the initialization phase.
623 *
624 * @return accelerometer base noise level of the best solution that has been
625 * found.
626 */
627 public double getAccelerometerBaseNoiseLevel() {
628 return baseNoiseLevel;
629 }
630
631 /**
632 * Gets accelerometer base noise level that has been detected during
633 * initialization of the best solution that has been found.
634 * This is equal to the standard deviation of the accelerometer measurements
635 * during the initialization phase.
636 *
637 * @return accelerometer base noise level of the best solution that has been
638 * found.
639 */
640 public Acceleration getAccelerometerBaseNoiseLevelAsMeasurement() {
641 return createMeasurement(baseNoiseLevel, getDefaultUnit());
642 }
643
644 /**
645 * Gets accelerometer base noise level that has been detected during
646 * initialization of the best solution that has been found.
647 * This is equal to the standard deviation of the accelerometer measurements
648 * during the initialization phase.
649 *
650 * @param result instance where the result will be stored.
651 */
652 public void getAccelerometerBaseNoiseLevelAsMeasurement(final Acceleration result) {
653 result.setValue(baseNoiseLevel);
654 result.setUnit(getDefaultUnit());
655 }
656
657 /**
658 * Gets accelerometer base noise level PSD (Power Spectral Density)
659 * expressed in (m^2 * s^-3) of the best solution that has been found.
660 *
661 * @return accelerometer base noise level PSD of the best solution that has
662 * been found.
663 */
664 public double getAccelerometerBaseNoiseLevelPsd() {
665 return baseNoiseLevel * baseNoiseLevel * getTimeInterval();
666 }
667
668 /**
669 * Gets accelerometer base noise level root PSD (Power Spectral Density)
670 * expressed in (m * s^-1.5) of the best solution that has been found.
671 *
672 * @return accelerometer base noise level root PSD of the best solution that has
673 * been found.
674 */
675 @Override
676 public double getAccelerometerBaseNoiseLevelRootPsd() {
677 return baseNoiseLevel * Math.sqrt(getTimeInterval());
678 }
679
680 /**
681 * Gets the threshold to determine static/dynamic period changes expressed in
682 * meters per squared second (m/s^2) for the best calibration solution that
683 * has been found.
684 *
685 * @return threshold to determine static/dynamic period changes for the best
686 * solution.
687 */
688 public double getThreshold() {
689 return threshold;
690 }
691
692 /**
693 * Gets the threshold to determine static/dynamic period changes for the best
694 * calibration solution that has been found.
695 *
696 * @return threshold to determine static/dynamic period changes for the best
697 * solution.
698 */
699 public Acceleration getThresholdAsMeasurement() {
700 return createMeasurement(threshold, getDefaultUnit());
701 }
702
703 /**
704 * Get the threshold to determine static/dynamic period changes for the best
705 * calibration solution that has been found.
706 *
707 * @param result instance where the result will be stored.
708 */
709 public void getThresholdAsMeasurement(final Acceleration result) {
710 result.setValue(threshold);
711 result.setUnit(getDefaultUnit());
712 }
713
714 /**
715 * Gets the array containing x,y,z components of estimated magnetometer
716 * hard-iron biases expressed in Teslas (T).
717 *
718 * @return array containing x,y,z components of estimated magnetometer
719 * hard-iron biases.
720 */
721 public double[] getEstimatedHardIron() {
722 return estimatedHardIron;
723 }
724
725 /**
726 * Gets estimated magnetometer soft-iron matrix containing scale factors
727 * and cross-coupling errors.
728 * This is the product of matrix Tm containing cross-coupling errors and Km
729 * containing scaling factors.
730 * So that:
731 * <pre>
732 * Mm = [sx mxy mxz] = Tm*Km
733 * [myx sy myz]
734 * [mzx mzy sz ]
735 * </pre>
736 * Where:
737 * <pre>
738 * Km = [sx 0 0 ]
739 * [0 sy 0 ]
740 * [0 0 sz]
741 * </pre>
742 * and
743 * <pre>
744 * Tm = [1 -alphaXy alphaXz ]
745 * [alphaYx 1 -alphaYz]
746 * [-alphaZx alphaZy 1 ]
747 * </pre>
748 * Hence:
749 * <pre>
750 * Mm = [sx mxy mxz] = Tm*Km = [sx -sy * alphaXy sz * alphaXz ]
751 * [myx sy myz] [sx * alphaYx sy -sz * alphaYz]
752 * [mzx mzy sz ] [-sx * alphaZx sy * alphaZy sz ]
753 * </pre>
754 * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
755 * are considered to be zero if the accelerometer z-axis is assumed to be the same
756 * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mm matrix
757 * becomes upper diagonal:
758 * <pre>
759 * Mm = [sx mxy mxz]
760 * [0 sy myz]
761 * [0 0 sz ]
762 * </pre>
763 * Values of this matrix are unit-less.
764 *
765 * @return estimated magnetometer soft-iron scale factors and cross-coupling errors,
766 * or null if not available.
767 */
768 public Matrix getEstimatedMm() {
769 return estimatedMm;
770 }
771
772 /**
773 * Evaluates calibration Mean Square Error (MSE) for the provided threshold factor.
774 *
775 * @param thresholdFactor threshold factor to be used for interval detection
776 * and measurement generation to be used for
777 * calibration.
778 * @return calibration MSE.
779 * @throws LockedException if the generator is busy.
780 * @throws CalibrationException if calibration fails.
781 * @throws NotReadyException if the calibrator is not ready.
782 * @throws IntervalDetectorThresholdFactorOptimizerException interval detection failed.
783 */
784 protected double evaluateForThresholdFactor(final double thresholdFactor) throws LockedException,
785 CalibrationException, NotReadyException, IntervalDetectorThresholdFactorOptimizerException {
786 if (measurements == null) {
787 measurements = new ArrayList<>();
788 } else {
789 measurements.clear();
790 }
791
792 generator.reset();
793 generator.setThresholdFactor(thresholdFactor);
794
795 var count = dataSource.count();
796 var failed = false;
797 for (var i = 0; i < count; i++) {
798 final var bodyKinematics = dataSource.getAt(i);
799 if (!generator.process(bodyKinematics)) {
800 failed = true;
801 break;
802 }
803 }
804
805 if (failed || generator.getStatus() == TriadStaticIntervalDetector.Status.FAILED) {
806 // interval detection failed
807 return Double.MAX_VALUE;
808 }
809
810 // check that enough measurements have been obtained
811 if (measurements.size() < calibrator.getMinimumRequiredMeasurements()) {
812 return Double.MAX_VALUE;
813 }
814
815 // set calibrator measurements
816 switch (calibrator.getMeasurementType()) {
817 case STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY:
818 if (calibrator.isOrderedMeasurementsRequired()) {
819 final var cal =
820 (OrderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator) this.calibrator;
821
822 cal.setMeasurements(measurements);
823 } else {
824 final var cal =
825 (UnorderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator) this.calibrator;
826
827 cal.setMeasurements(measurements);
828 }
829
830 if (calibrator.isQualityScoresRequired()) {
831 final var cal = (QualityScoredMagnetometerCalibrator) this.calibrator;
832
833 final var size = measurements.size();
834 final var qualityScores = new double[size];
835 for (var i = 0; i < size; i++) {
836 qualityScores[i] = qualityScoreMapper.map(measurements.get(i));
837 }
838 cal.setQualityScores(qualityScores);
839 }
840 break;
841 case FRAME_BODY_MAGNETIC_FLUX_DENSITY, STANDARD_DEVIATION_FRAME_BODY_MAGNETIC_FLUX_DENSITY:
842 // Throw exception. Cannot use frames
843 default:
844 throw new IntervalDetectorThresholdFactorOptimizerException();
845 }
846
847 calibrator.calibrate();
848
849 final var mse = calibrator.getEstimatedMse();
850 if (mse < minMse) {
851 keepBestResult(mse, thresholdFactor);
852 }
853 return mse;
854 }
855
856 /**
857 * Initializes magnetometer measurement generator to convert
858 * body kinematics and magnetic flux measurements after interval detection into
859 * measurements used for magnetometer calibration.
860 */
861 private void initialize() {
862 final var generatorListener = new MagnetometerMeasurementsGeneratorListener() {
863 @Override
864 public void onInitializationStarted(final MagnetometerMeasurementsGenerator generator) {
865 // not needed
866 }
867
868 @Override
869 public void onInitializationCompleted(
870 final MagnetometerMeasurementsGenerator generator, final double baseNoiseLevel) {
871 // not needed
872 }
873
874 @Override
875 public void onError(
876 final MagnetometerMeasurementsGenerator generator,
877 final TriadStaticIntervalDetector.ErrorReason reason) {
878 // not needed
879 }
880
881 @Override
882 public void onStaticIntervalDetected(final MagnetometerMeasurementsGenerator generator) {
883 // not needed
884 }
885
886 @Override
887 public void onDynamicIntervalDetected(final MagnetometerMeasurementsGenerator generator) {
888 // not needed
889 }
890
891 @Override
892 public void onStaticIntervalSkipped(final MagnetometerMeasurementsGenerator generator) {
893 // not needed
894 }
895
896 @Override
897 public void onDynamicIntervalSkipped(final MagnetometerMeasurementsGenerator generator) {
898 // not needed
899 }
900
901 @Override
902 public void onGeneratedMeasurement(
903 final MagnetometerMeasurementsGenerator generator,
904 final StandardDeviationBodyMagneticFluxDensity measurement) {
905 measurements.add(measurement);
906 }
907
908 @Override
909 public void onReset(final MagnetometerMeasurementsGenerator generator) {
910 // not needed
911 }
912 };
913
914 generator = new MagnetometerMeasurementsGenerator(generatorListener);
915 }
916
917 /**
918 * Keeps the best calibration solution found so far.
919 *
920 * @param mse Estimated Mean Square Error during calibration.
921 * @param thresholdFactor threshold factor to be kept.
922 */
923 private void keepBestResult(final double mse, final double thresholdFactor) {
924 minMse = mse;
925 optimalThresholdFactor = thresholdFactor;
926
927 baseNoiseLevel = generator.getAccelerometerBaseNoiseLevel();
928 threshold = generator.getThreshold();
929
930 if (estimatedCovariance == null) {
931 estimatedCovariance = new Matrix(calibrator.getEstimatedCovariance());
932 } else {
933 estimatedCovariance.copyFrom(calibrator.getEstimatedCovariance());
934 }
935 if (estimatedMm == null) {
936 estimatedMm = new Matrix(calibrator.getEstimatedMm());
937 } else {
938 estimatedMm.copyFrom(calibrator.getEstimatedMm());
939 }
940 if (calibrator instanceof UnknownHardIronMagnetometerCalibrator unknownHardIronMagnetometerCalibrator) {
941 estimatedHardIron = unknownHardIronMagnetometerCalibrator.getEstimatedHardIron();
942 } else if (calibrator instanceof KnownHardIronMagnetometerCalibrator knownHardIronMagnetometerCalibrator) {
943 estimatedHardIron = knownHardIronMagnetometerCalibrator.getHardIron();
944 }
945 }
946
947 /**
948 * Creates an acceleration instance using the provided value and unit.
949 *
950 * @param value value of measurement.
951 * @param unit unit of value.
952 * @return created acceleration.
953 */
954 private Acceleration createMeasurement(final double value, final AccelerationUnit unit) {
955 return new Acceleration(value, unit);
956 }
957
958 /**
959 * Gets the default unit for acceleration, which is meters per
960 * squared second (m/s^2).
961 *
962 * @return default unit for acceleration.
963 */
964 private AccelerationUnit getDefaultUnit() {
965 return AccelerationUnit.METERS_PER_SQUARED_SECOND;
966 }
967 }