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