AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer.java

/*
 * Copyright (C) 2021 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.navigation.inertial.calibration.intervals.thresholdfactor;

import com.irurueta.algebra.Matrix;
import com.irurueta.navigation.LockedException;
import com.irurueta.navigation.NotReadyException;
import com.irurueta.navigation.inertial.INSLooselyCoupledKalmanInitializerConfig;
import com.irurueta.navigation.inertial.INSTightlyCoupledKalmanInitializerConfig;
import com.irurueta.navigation.inertial.calibration.AccelerometerNoiseRootPsdSource;
import com.irurueta.navigation.inertial.calibration.BodyKinematicsSequence;
import com.irurueta.navigation.inertial.calibration.CalibrationException;
import com.irurueta.navigation.inertial.calibration.GyroscopeNoiseRootPsdSource;
import com.irurueta.navigation.inertial.calibration.StandardDeviationBodyKinematics;
import com.irurueta.navigation.inertial.calibration.StandardDeviationBodyMagneticFluxDensity;
import com.irurueta.navigation.inertial.calibration.StandardDeviationTimedBodyKinematics;
import com.irurueta.navigation.inertial.calibration.TimedBodyKinematicsAndMagneticFluxDensity;
import com.irurueta.navigation.inertial.calibration.accelerometer.AccelerometerCalibratorMeasurementType;
import com.irurueta.navigation.inertial.calibration.accelerometer.AccelerometerNonLinearCalibrator;
import com.irurueta.navigation.inertial.calibration.accelerometer.KnownBiasAccelerometerCalibrator;
import com.irurueta.navigation.inertial.calibration.accelerometer.OrderedStandardDeviationBodyKinematicsAccelerometerCalibrator;
import com.irurueta.navigation.inertial.calibration.accelerometer.QualityScoredAccelerometerCalibrator;
import com.irurueta.navigation.inertial.calibration.accelerometer.UnknownBiasAccelerometerCalibrator;
import com.irurueta.navigation.inertial.calibration.accelerometer.UnorderedStandardDeviationBodyKinematicsAccelerometerCalibrator;
import com.irurueta.navigation.inertial.calibration.generators.AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator;
import com.irurueta.navigation.inertial.calibration.generators.AccelerometerGyroscopeAndMagnetometerMeasurementsGeneratorListener;
import com.irurueta.navigation.inertial.calibration.gyroscope.AccelerometerDependentGyroscopeCalibrator;
import com.irurueta.navigation.inertial.calibration.gyroscope.GyroscopeCalibratorMeasurementOrSequenceType;
import com.irurueta.navigation.inertial.calibration.gyroscope.GyroscopeNonLinearCalibrator;
import com.irurueta.navigation.inertial.calibration.gyroscope.OrderedBodyKinematicsSequenceGyroscopeCalibrator;
import com.irurueta.navigation.inertial.calibration.gyroscope.QualityScoredGyroscopeCalibrator;
import com.irurueta.navigation.inertial.calibration.gyroscope.UnknownBiasGyroscopeCalibrator;
import com.irurueta.navigation.inertial.calibration.intervals.TriadStaticIntervalDetector;
import com.irurueta.navigation.inertial.calibration.magnetometer.KnownHardIronMagnetometerCalibrator;
import com.irurueta.navigation.inertial.calibration.magnetometer.MagnetometerCalibratorMeasurementType;
import com.irurueta.navigation.inertial.calibration.magnetometer.MagnetometerNonLinearCalibrator;
import com.irurueta.navigation.inertial.calibration.magnetometer.OrderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator;
import com.irurueta.navigation.inertial.calibration.magnetometer.QualityScoredMagnetometerCalibrator;
import com.irurueta.navigation.inertial.calibration.magnetometer.UnknownHardIronMagnetometerCalibrator;
import com.irurueta.navigation.inertial.calibration.magnetometer.UnorderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator;
import com.irurueta.units.Acceleration;
import com.irurueta.units.AccelerationUnit;
import com.irurueta.units.Time;

import java.util.ArrayList;
import java.util.List;

/**
 * Optimizes the threshold factor for interval detection of accelerometer and gyroscope
 * data based on results of calibration.
 * Implementations of this class will attempt to find the best threshold factor
 * between the provided range of values.
 * Only accelerometer calibrators based on unknown orientation are supported (in other terms,
 * calibrators must be {@link AccelerometerNonLinearCalibrator} and must support
 * {@link AccelerometerCalibratorMeasurementType#STANDARD_DEVIATION_BODY_KINEMATICS}).
 * Only gyroscope calibrators based on unknown orientation are supported (in other terms,
 * calibrators must be {@link GyroscopeNonLinearCalibrator} and must support
 * {@link GyroscopeCalibratorMeasurementOrSequenceType#BODY_KINEMATICS_SEQUENCE}).
 * Only magnetometer calibrators based on unknown orientation are supported, in other terms,
 * calibrators must be {@link MagnetometerNonLinearCalibrator} and must support
 * {@link MagnetometerCalibratorMeasurementType#STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY}.
 */
public abstract class AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer extends
        IntervalDetectorThresholdFactorOptimizer<TimedBodyKinematicsAndMagneticFluxDensity,
                AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizerDataSource> implements
        AccelerometerNoiseRootPsdSource, GyroscopeNoiseRootPsdSource {

    /**
     * Default minimum threshold factor.
     */
    public static final double DEFAULT_MIN_THRESHOLD_FACTOR = 2.0;

    /**
     * Default maximum threshold factor.
     */
    public static final double DEFAULT_MAX_THRESHOLD_FACTOR = 10.0;

    /**
     * Minimum threshold factor.
     */
    protected double minThresholdFactor = DEFAULT_MIN_THRESHOLD_FACTOR;

    /**
     * Maximum threshold factor.
     */
    protected double maxThresholdFactor = DEFAULT_MAX_THRESHOLD_FACTOR;

    /**
     * Accelerometer calibrator.
     */
    private AccelerometerNonLinearCalibrator accelerometerCalibrator;

    /**
     * Gyroscope calibrator.
     */
    private GyroscopeNonLinearCalibrator gyroscopeCalibrator;

    /**
     * Magnetometer calibrator.
     */
    private MagnetometerNonLinearCalibrator magnetometerCalibrator;

    /**
     * A measurement generator for accelerometer, gyroscope and magnetometer calibrators.
     */
    private AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator;

    /**
     * Generated measurements to be used for accelerometer calibration.
     */
    private List<StandardDeviationBodyKinematics> accelerometerMeasurements;

    /**
     * Generated sequences to be used for gyroscope calibration.
     */
    private List<BodyKinematicsSequence<StandardDeviationTimedBodyKinematics>> gyroscopeSequences;

    /**
     * Generated measurements to be used for magnetometer calibration.
     */
    private List<StandardDeviationBodyMagneticFluxDensity> magnetometerMeasurements;

    /**
     * Mapper to convert {@link StandardDeviationBodyKinematics} measurements into
     * quality scores.
     */
    private QualityScoreMapper<StandardDeviationBodyKinematics> accelerometerQualityScoreMapper =
            new DefaultAccelerometerQualityScoreMapper();

    /**
     * Mapper to convert {@link BodyKinematicsSequence} sequences of {@link StandardDeviationTimedBodyKinematics}
     * into quality scores.
     */
    private QualityScoreMapper<BodyKinematicsSequence<StandardDeviationTimedBodyKinematics>>
            gyroscopeQualityScoreMapper = new DefaultGyroscopeQualityScoreMapper();

    /**
     * Mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} measurements
     * into quality scores.
     */
    private QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> magnetometerQualityScoreMapper =
            new DefaultMagnetometerQualityScoreMapper();

    /**
     * Rule to convert accelerometer, gyroscope and magnetometer MSE
     * values into a single global MSE value.
     */
    private AccelerometerGyroscopeAndMagnetometerMseRule mseRule =
            new DefaultAccelerometerGyroscopeAndMagnetometerMseRule();

    /**
     * Estimated norm of gyroscope noise root PSD (Power Spectral Density)
     * expressed as (rad * s^-0.5).
     */
    private double angularSpeedNoiseRootPsd;

    /**
     * Accelerometer base noise level that has been detected during
     * initialization of the best solution that has been found expressed in
     * meters per squared second (m/s^2).
     * This is equal to the standard deviation of the accelerometer measurements
     * during the initialization phase.
     */
    private double baseNoiseLevel;

    /**
     * Threshold to determine static/dynamic period changes expressed in
     * meters per squared second (m/s^2) for the best calibration solution that
     * has been found.
     */
    private double threshold;

    /**
     * Estimated covariance matrix for estimated accelerometer parameters.
     */
    private Matrix estimatedAccelerometerCovariance;

    /**
     * Estimated accelerometer scale factors and cross-coupling errors.
     * This is the product of matrix Ta containing cross-coupling errors and Ka
     * containing scaling factors.
     * So that:
     * <pre>
     *     Ma = [sx    mxy  mxz] = Ta*Ka
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Ka = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Ta = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Ma = [sx    mxy  mxz] = Ta*Ka =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the accelerometer z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Ma matrix
     * becomes upper diagonal:
     * <pre>
     *     Ma = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     */
    private Matrix estimatedAccelerometerMa;

    /**
     * Estimated accelerometer biases for each IMU axis expressed in meter per squared
     * second (m/s^2).
     */
    private double[] estimatedAccelerometerBiases;

    /**
     * Estimated covariance matrix for estimated gyroscope parameters.
     */
    private Matrix estimatedGyroscopeCovariance;

    /**
     * Estimated angular rate biases for each IMU axis expressed in radians per
     * second (rad/s).
     */
    private double[] estimatedGyroscopeBiases;

    /**
     * Estimated gyroscope scale factors and cross-coupling errors.
     * This is the product of matrix Tg containing cross-coupling errors and Kg
     * containing scaling factors.
     * So that:
     * <pre>
     *     Mg = [sx    mxy  mxz] = Tg*Kg
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Kg = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Tg = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Mg = [sx    mxy  mxz] = Tg*Kg =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the gyroscope z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mg matrix
     * becomes upper diagonal:
     * <pre>
     *     Mg = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     */
    private Matrix estimatedGyroscopeMg;

    /**
     * Estimated G-dependent cross-biases introduced on the gyroscope by the
     * specific forces sensed by the accelerometer.
     * This instance allows any 3x3 matrix.
     */
    private Matrix estimatedGyroscopeGg;

    /**
     * Estimated covariance matrix for estimated magnetometer parameters.
     */
    private Matrix estimatedMagnetometerCovariance;

    /**
     * Estimated magnetometer soft-iron matrix containing scale factors
     * and cross-coupling errors.
     * This is the product of matrix Tm containing cross-coupling errors and Km
     * containing scaling factors.
     * So that:
     * <pre>
     *     Mm = [sx    mxy  mxz] = Tm*Km
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Km = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Tm = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Mm = [sx    mxy  mxz] = Tm*Km =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the accelerometer z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mm matrix
     * becomes upper diagonal:
     * <pre>
     *     Mm = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     */
    private Matrix estimatedMagnetometerMm;

    /**
     * Estimated magnetometer hard-iron biases for each magnetometer axis
     * expressed in Teslas (T).
     */
    private double[] estimatedMagnetometerHardIron;

    /**
     * Constructor.
     */
    protected AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer() {
        initialize();
    }

    /**
     * Constructor.
     *
     * @param dataSource instance in charge of retrieving data for this optimizer.
     */
    protected AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer(
            final AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizerDataSource dataSource) {
        super(dataSource);
        initialize();
    }

    /**
     * Constructor.
     *
     * @param accelerometerCalibrator an accelerometer calibrator to be used to
     *                                optimize its Mean Square Error (MSE).
     * @param gyroscopeCalibrator     a gyroscope calibrator to be used to optimize
     *                                its Mean Square Error (MSE).
     * @param magnetometerCalibrator  a magnetometer calibrator to be used to
     *                                optimize its Mean Square Error (MSE).
     * @throws IllegalArgumentException if accelerometer calibrator does not use
     *                                  {@link StandardDeviationBodyKinematics} measurements,
     *                                  if gyroscope calibrator does not use
     *                                  {@link BodyKinematicsSequence} sequences of
     *                                  {@link StandardDeviationTimedBodyKinematics} or
     *                                  if magnetometer calibrator does not use
     *                                  {@link StandardDeviationBodyMagneticFluxDensity} measurements.
     */
    protected AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer(
            final AccelerometerNonLinearCalibrator accelerometerCalibrator,
            final GyroscopeNonLinearCalibrator gyroscopeCalibrator,
            final MagnetometerNonLinearCalibrator magnetometerCalibrator) {
        try {
            setAccelerometerCalibrator(accelerometerCalibrator);
            setGyroscopeCalibrator(gyroscopeCalibrator);
            setMagnetometerCalibrator(magnetometerCalibrator);
        } catch (final LockedException ignore) {
            // never happens
        }
        initialize();
    }

    /**
     * Constructor.
     *
     * @param dataSource              instance in charge of retrieving data for this optimizer.
     * @param accelerometerCalibrator an accelerometer calibrator to be used to
     *                                optimize its Mean Square Error (MSE).
     * @param gyroscopeCalibrator     a gyroscope calibrator to be used to optimize
     *                                its Mean Square Error (MSE).
     * @param magnetometerCalibrator  a magnetometer calibrator to be used to
     *                                optimize its Mean Square Error (MSE).
     * @throws IllegalArgumentException if accelerometer calibrator does not use
     *                                  {@link StandardDeviationBodyKinematics} measurements,
     *                                  if gyroscope calibrator does not use
     *                                  {@link BodyKinematicsSequence} sequences of
     *                                  {@link StandardDeviationTimedBodyKinematics} or
     *                                  if magnetometer calibrator does not use
     *                                  {@link StandardDeviationBodyMagneticFluxDensity} measurements.
     */
    protected AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizer(
            final AccelerometerGyroscopeAndMagnetometerIntervalDetectorThresholdFactorOptimizerDataSource dataSource,
            final AccelerometerNonLinearCalibrator accelerometerCalibrator,
            final GyroscopeNonLinearCalibrator gyroscopeCalibrator,
            final MagnetometerNonLinearCalibrator magnetometerCalibrator) {
        super(dataSource);
        try {
            setAccelerometerCalibrator(accelerometerCalibrator);
            setGyroscopeCalibrator(gyroscopeCalibrator);
            setMagnetometerCalibrator(magnetometerCalibrator);
        } catch (final LockedException ignore) {
            // never happens
        }
        initialize();
    }

    /**
     * Gets provided accelerometer calibrator to be used to optimize its Mean Square Error (MSE).
     *
     * @return accelerometer calibrator to be used to optimize its MSE.
     */
    public AccelerometerNonLinearCalibrator getAccelerometerCalibrator() {
        return accelerometerCalibrator;
    }

    /**
     * Sets accelerometer calibrator to be used to optimize its Mean Square Error (MSE).
     *
     * @param accelerometerCalibrator accelerometer calibrator to be used to optimize its MSE.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if accelerometer calibrator does not use
     *                                  {@link StandardDeviationBodyKinematics} measurements.
     */
    public void setAccelerometerCalibrator(
            final AccelerometerNonLinearCalibrator accelerometerCalibrator) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        if (accelerometerCalibrator != null && accelerometerCalibrator.getMeasurementType()
                != AccelerometerCalibratorMeasurementType.STANDARD_DEVIATION_BODY_KINEMATICS) {
            throw new IllegalArgumentException();
        }

        this.accelerometerCalibrator = accelerometerCalibrator;
    }

    /**
     * Gets provided gyroscope calibrator to be used to optimize its Mean Square Error (MSE).
     *
     * @return gyroscope calibrator to be used to optimize its MSE.
     */
    public GyroscopeNonLinearCalibrator getGyroscopeCalibrator() {
        return gyroscopeCalibrator;
    }

    /**
     * Sets gyroscope calibrator to be used to optimize its Mean Square Error (MSE).
     *
     * @param gyroscopeCalibrator gyroscope calibrator to be use dto optimize its MSE.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if gyroscope calibrator does not use
     *                                  {@link BodyKinematicsSequence} sequences of
     *                                  {@link StandardDeviationTimedBodyKinematics}.
     */
    public void setGyroscopeCalibrator(final GyroscopeNonLinearCalibrator gyroscopeCalibrator) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        if (gyroscopeCalibrator != null && gyroscopeCalibrator.getMeasurementOrSequenceType()
                != GyroscopeCalibratorMeasurementOrSequenceType.BODY_KINEMATICS_SEQUENCE) {
            throw new IllegalArgumentException();
        }

        this.gyroscopeCalibrator = gyroscopeCalibrator;
    }

    /**
     * Gets provided magnetometer calibrator to be used to optimize its Mean Square Error (MSE).
     *
     * @return magnetometer calibrator to be used to optimize its MSE.
     */
    public MagnetometerNonLinearCalibrator getMagnetometerCalibrator() {
        return magnetometerCalibrator;
    }

    /**
     * Sets a magnetometer calibrator to be used to optimize its Mean Square Error.
     *
     * @param magnetometerCalibrator magnetometer calibrator to be used to optimize its MSE.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if magnetometer calibrator does not use
     *                                  {@link StandardDeviationBodyMagneticFluxDensity} measurements.
     */
    public void setMagnetometerCalibrator(final MagnetometerNonLinearCalibrator magnetometerCalibrator)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }

        if (magnetometerCalibrator != null && magnetometerCalibrator.getMeasurementType()
                != MagnetometerCalibratorMeasurementType.STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY) {
            throw new IllegalArgumentException();
        }

        this.magnetometerCalibrator = magnetometerCalibrator;
    }

    /**
     * Gets mapper to convert {@link StandardDeviationBodyKinematics} accelerometer measurements
     * into quality scores.
     *
     * @return mapper to convert accelerometer measurements into quality scores.
     */
    public QualityScoreMapper<StandardDeviationBodyKinematics> getAccelerometerQualityScoreMapper() {
        return accelerometerQualityScoreMapper;
    }

    /**
     * Sets mapper to convert {@link StandardDeviationBodyKinematics} accelerometer measurements
     * into quality scores.
     *
     * @param accelerometerQualityScoreMapper mapper to convert accelerometer measurements into
     *                                        quality scores.
     * @throws LockedException if optimizer is already running.
     */
    public void setAccelerometerQualityScoreMapper(
            final QualityScoreMapper<StandardDeviationBodyKinematics> accelerometerQualityScoreMapper)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }

        this.accelerometerQualityScoreMapper = accelerometerQualityScoreMapper;
    }

    /**
     * Gets mapper to convert {@link BodyKinematicsSequence} gyroscope sequences of
     * {@link StandardDeviationTimedBodyKinematics} into quality scores.
     *
     * @return mapper to convert gyroscope sequences into quality scores.
     */
    public QualityScoreMapper<BodyKinematicsSequence<StandardDeviationTimedBodyKinematics>>
    getGyroscopeQualityScoreMapper() {
        return gyroscopeQualityScoreMapper;
    }

    /**
     * Sets mapper to convert {@link BodyKinematicsSequence} gyroscope sequences of
     * {@link StandardDeviationTimedBodyKinematics} into quality scores.
     *
     * @param gyroscopeQualityScoreMapper mapper to convert gyroscope sequences
     *                                    into quality scores.
     * @throws LockedException if optimizer is already running.
     */
    public void setGyroscopeQualityScoreMapper(
            final QualityScoreMapper<BodyKinematicsSequence<StandardDeviationTimedBodyKinematics>>
                    gyroscopeQualityScoreMapper) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        this.gyroscopeQualityScoreMapper = gyroscopeQualityScoreMapper;
    }

    /**
     * Gets mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} magnetometer
     * measurements into quality scores.
     *
     * @return mapper to convert magnetometer measurements into quality scores.
     */
    public QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> getMagnetometerQualityScoreMapper() {
        return magnetometerQualityScoreMapper;
    }

    /**
     * Sets mapper to convert {@link StandardDeviationBodyMagneticFluxDensity} magnetometer
     * measurements into quality scores.
     *
     * @param magnetometerQualityScoreMapper mapper to convert magnetometer measurements into
     *                                       quality scores.
     * @throws LockedException if optimizer is already running.
     */
    public void setMagnetometerQualityScoreMapper(
            final QualityScoreMapper<StandardDeviationBodyMagneticFluxDensity> magnetometerQualityScoreMapper)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }

        this.magnetometerQualityScoreMapper = magnetometerQualityScoreMapper;
    }

    /**
     * Gets rule to convert accelerometer, gyroscope and magnetometer
     * MSE values into a single global MSE value.
     *
     * @return rule to convert accelerometer, gyroscope and
     * magnetometer MSE values into a single global MSE value.
     */
    public AccelerometerGyroscopeAndMagnetometerMseRule getMseRule() {
        return mseRule;
    }

    /**
     * Sets rule to convert accelerometer, gyroscope and magnetometer
     * MSE values into a single global MSE value.
     *
     * @param mseRule rule to convert accelerometer, gyroscope and
     *                magnetometer MSE values into a single global
     *                MSE value.
     * @throws LockedException if optimizer is already running.
     */
    public void setMseRule(final AccelerometerGyroscopeAndMagnetometerMseRule mseRule) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        this.mseRule = mseRule;
    }

    /**
     * Gets the minimum threshold factor.
     *
     * @return minimum threshold factor.
     */
    public double getMinThresholdFactor() {
        return minThresholdFactor;
    }

    /**
     * Gets the maximum threshold factor.
     *
     * @return maximum threshold factor.
     */
    public double getMaxThresholdFactor() {
        return maxThresholdFactor;
    }

    /**
     * Sets a range of threshold factor values to get an optimized
     * threshold factor value.
     *
     * @param minThresholdFactor minimum threshold.
     * @param maxThresholdFactor maximum threshold.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if either minimum or maximum values are
     *                                  negative, or if the minimum value is larger
     *                                  than the maximum one.
     */
    public void setThresholdFactorRange(final double minThresholdFactor, final double maxThresholdFactor)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }
        if (minThresholdFactor < 0.0 || maxThresholdFactor < 0.0 || minThresholdFactor >= maxThresholdFactor) {
            throw new IllegalArgumentException();
        }

        this.minThresholdFactor = minThresholdFactor;
        this.maxThresholdFactor = maxThresholdFactor;
    }

    /**
     * Indicates whether this optimizer is ready to start optimization.
     *
     * @return true if this optimizer is ready, false otherwise.
     */
    @Override
    public boolean isReady() {
        return super.isReady() && accelerometerCalibrator != null
                && gyroscopeCalibrator != null
                && magnetometerCalibrator != null
                && accelerometerQualityScoreMapper != null
                && gyroscopeQualityScoreMapper != null
                && magnetometerQualityScoreMapper != null
                && mseRule != null;
    }

    /**
     * Gets the time interval between input measurements provided to the
     * {@link #getDataSource()} expressed in seconds (s).
     *
     * @return time interval between input measurements.
     */
    public double getTimeInterval() {
        return generator.getTimeInterval();
    }

    /**
     * Sets time interval between input measurements provided to the
     * {@link #getDataSource()} expressed in seconds (s).
     *
     * @param timeInterval time interval between input measurements.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is negative.
     */
    public void setTimeInterval(final double timeInterval) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setTimeInterval(timeInterval);
    }

    /**
     * Gets the time interval between input measurements provided to the
     * {@link #getDataSource()}.
     *
     * @return time interval between input measurements.
     */
    public Time getTimeIntervalAsTime() {
        return generator.getTimeIntervalAsTime();
    }

    /**
     * Gets the time interval between input measurements provided to the
     * {@link #getDataSource()}.
     *
     * @param result instance where the time interval will be stored.
     */
    public void getTimeIntervalAsTime(final Time result) {
        generator.getTimeIntervalAsTime(result);
    }

    /**
     * Sets time interval between input measurements provided to the
     * {@link #getDataSource()}.
     *
     * @param timeInterval time interval between input measurements.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is negative.
     */
    public void setTimeInterval(final Time timeInterval) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setTimeInterval(timeInterval);
    }

    /**
     * Gets minimum number of input measurements provided to the
     * {@link #getDataSource()} required in a static interval to be taken
     * into account.
     * Smaller static intervals will be discarded.
     *
     * @return a minimum number of input measurements required in a static interval
     * to be taken into account.
     */
    public int getMinStaticSamples() {
        return generator.getMinStaticSamples();
    }

    /**
     * Sets minimum number of input measurements provided to the
     * {@link #getDataSource()} required in a static interval to be taken
     * into account.
     * Smaller static intervals will be discarded.
     *
     * @param minStaticSamples a minimum number of input measurements required in
     *                         a static interval to be taken into account.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is less than 2.
     */
    public void setMinStaticSamples(final int minStaticSamples) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setMinStaticSamples(minStaticSamples);
    }

    /**
     * Gets maximum number of input measurements provided to the
     * {@link #getDataSource()} allowed in dynamic intervals.
     *
     * @return maximum number of input measurements allowed in dynamic intervals.
     */
    public int getMaxDynamicSamples() {
        return generator.getMaxDynamicSamples();
    }

    /**
     * Sets maximum number of input measurements provided to the
     * {@link #getDataSource()} allowed in dynamic intervals.
     *
     * @param maxDynamicSamples maximum number of input measurements allowed in
     *                          dynamic intervals.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is less than 2.
     */
    public void setMaxDynamicSamples(final int maxDynamicSamples) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setMaxDynamicSamples(maxDynamicSamples);
    }

    /**
     * Gets length of number of input measurements provided to the
     * {@link #getDataSource()} to keep within the window being processed
     * to determine instantaneous accelerometer noise level.
     *
     * @return length of input measurements to keep within the window.
     */
    public int getWindowSize() {
        return generator.getWindowSize();
    }

    /**
     * Sets length of number of input measurements provided to the
     * {@link #getDataSource()} to keep within the window being processed
     * to determine instantaneous noise level.
     * Window size must always be larger than the allowed minimum value, which is 2
     * and must have an odd value.
     *
     * @param windowSize length of number of samples to keep within the window.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is not valid.
     */
    public void setWindowSize(final int windowSize) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setWindowSize(windowSize);
    }

    /**
     * Gets number of input measurements provided to the
     * {@link #getDataSource()} to be processed initially while keeping the sensor
     * static to find the base noise level when the device is static.
     *
     * @return number of samples to be processed initially.
     */
    public int getInitialStaticSamples() {
        return generator.getInitialStaticSamples();
    }

    /**
     * Sets number of input parameters provided to the {@link #getDataSource()}
     * to be processed initially while keeping the sensor static to
     * find the base noise level when the device is static.
     *
     * @param initialStaticSamples number of samples ot be processed initially.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is less than
     *                                  {@link TriadStaticIntervalDetector#MINIMUM_INITIAL_STATIC_SAMPLES}.
     */
    public void setInitialStaticSamples(final int initialStaticSamples) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setInitialStaticSamples(initialStaticSamples);
    }

    /**
     * Gets factor to determine that a sudden movement has occurred during
     * initialization if instantaneous noise level exceeds accumulated noise
     * level by this factor amount.
     * This factor is unit-less.
     *
     * @return factor to determine that a sudden movement has occurred.
     */
    public double getInstantaneousNoiseLevelFactor() {
        return generator.getInstantaneousNoiseLevelFactor();
    }

    /**
     * Sets factor to determine that a sudden movement has occurred during
     * initialization if instantaneous noise level exceeds accumulated noise
     * level by this factor amount.
     * This factor is unit-less.
     *
     * @param instantaneousNoiseLevelFactor factor to determine that a sudden
     *                                      movement has occurred during
     *                                      initialization.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is zero or negative.
     */
    public void setInstantaneousNoiseLevelFactor(final double instantaneousNoiseLevelFactor) throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setInstantaneousNoiseLevelFactor(instantaneousNoiseLevelFactor);
    }

    /**
     * Gets the overall absolute threshold to determine whether there has been
     * excessive motion during the whole initialization phase.
     * This threshold is expressed in meters per squared second (m/s^2).
     *
     * @return overall absolute threshold to determine whether there has been
     * excessive motion.
     */
    public double getBaseNoiseLevelAbsoluteThreshold() {
        return generator.getBaseNoiseLevelAbsoluteThreshold();
    }

    /**
     * Sets the overall absolute threshold to determine whether there has been
     * excessive motion during the whole initialization phase.
     * This threshold is expressed in meters per squared second (m/s^2).
     *
     * @param baseNoiseLevelAbsoluteThreshold overall absolute threshold to
     *                                        determine whether there has been
     *                                        excessive motion.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is zero or negative.
     */
    public void setBaseNoiseLevelAbsoluteThreshold(final double baseNoiseLevelAbsoluteThreshold)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setBaseNoiseLevelAbsoluteThreshold(baseNoiseLevelAbsoluteThreshold);
    }

    /**
     * Gets the overall absolute threshold to determine whether there has been
     * excessive motion during the whole initialization phase.
     *
     * @return overall absolute threshold to determine whether there has been
     * excessive motion.
     */
    public Acceleration getBaseNoiseLevelAbsoluteThresholdAsMeasurement() {
        return generator.getBaseNoiseLevelAbsoluteThresholdAsMeasurement();
    }

    /**
     * Gets the overall absolute threshold to determine whether there has been
     * excessive motion during the whole initialization phase.
     *
     * @param result instance where the result will be stored.
     */
    public void getBaseNoiseLevelAbsoluteThresholdAsMeasurement(final Acceleration result) {
        generator.getBaseNoiseLevelAbsoluteThresholdAsMeasurement(result);
    }

    /**
     * Sets the overall absolute threshold to determine whether there has been
     * excessive motion during the whole initialization phase.
     *
     * @param baseNoiseLevelAbsoluteThreshold overall absolute threshold to
     *                                        determine whether there has been
     *                                        excessive motion.
     * @throws LockedException          if optimizer is already running.
     * @throws IllegalArgumentException if provided value is zero or negative.
     */
    public void setBaseNoiseLevelAbsoluteThreshold(final Acceleration baseNoiseLevelAbsoluteThreshold)
            throws LockedException {
        if (running) {
            throw new LockedException();
        }

        generator.setBaseNoiseLevelAbsoluteThreshold(baseNoiseLevelAbsoluteThreshold);
    }

    /**
     * Gets accelerometer base noise level that has been detected during
     * initialization of the best solution that has been found expressed in
     * meters per squared second (m/s^2).
     * This is equal to the standard deviation of the accelerometer measurements
     * during the initialization phase.
     *
     * @return accelerometer base noise level of the best solution that has been
     * found.
     */
    public double getAccelerometerBaseNoiseLevel() {
        return baseNoiseLevel;
    }

    /**
     * Gets accelerometer base noise level that has been detected during
     * initialization of the best solution that has been found.
     * This is equal to the standard deviation of the accelerometer measurements
     * during the initialization phase.
     *
     * @return accelerometer base noise level of the best solution that has been
     * found.
     */
    public Acceleration getAccelerometerBaseNoiseLevelAsMeasurement() {
        return createMeasurement(baseNoiseLevel, getDefaultUnit());
    }

    /**
     * Gets accelerometer base noise level that has been detected during
     * initialization of the best solution that has been found.
     * This is equal to the standard deviation of the accelerometer measurements
     * during the initialization phase.
     *
     * @param result instance where the result will be stored.
     */
    public void getAccelerometerBaseNoiseLevelAsMeasurement(final Acceleration result) {
        result.setValue(baseNoiseLevel);
        result.setUnit(getDefaultUnit());
    }

    /**
     * Gets gyroscope base noise level PSD (Power Spectral Density)
     * expressed in (rad^2/s).
     *
     * @return gyroscope base noise level PSD.
     */
    public double getGyroscopeBaseNoiseLevelPsd() {
        return angularSpeedNoiseRootPsd * angularSpeedNoiseRootPsd;
    }

    /**
     * Gets gyroscope base noise level root PSD (Power Spectral Density)
     * expressed in (rad * s^-0.5)
     *
     * @return gyroscope base noise level root PSD.
     */
    @Override
    public double getGyroscopeBaseNoiseLevelRootPsd() {
        return angularSpeedNoiseRootPsd;
    }

    /**
     * Gets accelerometer base noise level PSD (Power Spectral Density)
     * expressed in (m^2 * s^-3).
     *
     * @return accelerometer base noise level PSD.
     */
    public double getAccelerometerBaseNoiseLevelPsd() {
        return baseNoiseLevel * baseNoiseLevel * getTimeInterval();
    }

    /**
     * Gets accelerometer base noise level root PSD (Power Spectral Density)
     * expressed in (m * s^-1.5).
     *
     * @return accelerometer base noise level root PSD.
     */
    @Override
    public double getAccelerometerBaseNoiseLevelRootPsd() {
        return baseNoiseLevel * Math.sqrt(getTimeInterval());
    }

    /**
     * Gets the threshold to determine static/dynamic period changes expressed in
     * meters per squared second (m/s^2) for the best calibration solution that
     * has been found.
     *
     * @return threshold to determine static/dynamic period changes for the best
     * solution.
     */
    public double getThreshold() {
        return threshold;
    }

    /**
     * Gets the threshold to determine static/dynamic period changes for the best
     * calibration solution that has been found.
     *
     * @return threshold to determine static/dynamic period changes for the best
     * solution.
     */
    public Acceleration getThresholdAsMeasurement() {
        return createMeasurement(threshold, getDefaultUnit());
    }

    /**
     * Get the threshold to determine static/dynamic period changes for the best
     * calibration solution that has been found.
     *
     * @param result instance where the result will be stored.
     */
    public void getThresholdAsMeasurement(final Acceleration result) {
        result.setValue(threshold);
        result.setUnit(getDefaultUnit());
    }

    /**
     * Gets estimated standard deviation norm of accelerometer bias expressed in
     * meters per squared second (m/s^2).
     * This can be used as the initial accelerometer bias uncertainty for
     * {@link INSLooselyCoupledKalmanInitializerConfig} or {@link INSTightlyCoupledKalmanInitializerConfig}.
     *
     * @return estimated standard deviation norm of accelerometer bias or null
     * if not available.
     */
    public Double getEstimatedAccelerometerBiasStandardDeviationNorm() {
        return estimatedAccelerometerCovariance != null
                ? Math.sqrt(getEstimatedAccelerometerBiasFxVariance()
                + getEstimatedAccelerometerBiasFyVariance()
                + getEstimatedAccelerometerBiasFzVariance())
                : null;
    }

    /**
     * Gets estimated x coordinate variance of accelerometer bias expressed in (m^2/s^4).
     *
     * @return estimated x coordinate variance of accelerometer bias or null if not available.
     */
    public Double getEstimatedAccelerometerBiasFxVariance() {
        return estimatedAccelerometerCovariance != null
                ? estimatedAccelerometerCovariance.getElementAt(0, 0) : null;
    }

    /**
     * Gets estimated y coordinate variance of accelerometer bias expressed in (m^2/s^4).
     *
     * @return estimated y coordinate variance of accelerometer bias or null if not available.
     */
    public Double getEstimatedAccelerometerBiasFyVariance() {
        return estimatedAccelerometerCovariance != null
                ? estimatedAccelerometerCovariance.getElementAt(1, 1) : null;
    }

    /**
     * Gets estimated z coordinate variance of accelerometer bias expressed in (m^2/s^4).
     *
     * @return estimated z coordinate variance of accelerometer bias or null if not available.
     */
    public Double getEstimatedAccelerometerBiasFzVariance() {
        return estimatedAccelerometerCovariance != null
                ? estimatedAccelerometerCovariance.getElementAt(2, 2) : null;
    }

    /**
     * Gets the array containing x,y,z components of estimated accelerometer biases
     * expressed in meters per squared second (m/s^2).
     *
     * @return array containing x,y,z components of estimated accelerometer biases.
     */
    public double[] getEstimatedAccelerometerBiases() {
        return estimatedAccelerometerBiases;
    }

    /**
     * Gets estimated accelerometer scale factors and cross-coupling errors.
     * This is the product of matrix Ta containing cross-coupling errors and Ka
     * containing scaling factors.
     * So that:
     * <pre>
     *     Ma = [sx    mxy  mxz] = Ta*Ka
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Ka = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Ta = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Ma = [sx    mxy  mxz] = Ta*Ka =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the accelerometer z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Ma matrix
     * becomes upper diagonal:
     * <pre>
     *     Ma = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     *
     * @return estimated accelerometer scale factors and cross-coupling errors, or null
     * if not available.
     */
    public Matrix getEstimatedAccelerometerMa() {
        return estimatedAccelerometerMa;
    }

    /**
     * Gets estimated standard deviation norm of gyroscope bias expressed in
     * radians per second (rad/s).
     * This can be used as the initial gyroscope bias uncertainty for
     * {@link INSLooselyCoupledKalmanInitializerConfig} or {@link INSTightlyCoupledKalmanInitializerConfig}.
     *
     * @return estimated standard deviation norm of gyroscope bias or null
     * if not available.
     */
    public Double getEstimatedGyroscopeBiasStandardDeviationNorm() {
        return estimatedGyroscopeCovariance != null
                ? Math.sqrt(getEstimatedGyroscopeBiasXVariance()
                + getEstimatedGyroscopeBiasYVariance()
                + getEstimatedGyroscopeBiasZVariance())
                : null;
    }

    /**
     * Gets estimated x coordinate variance of gyroscope bias expressed in (rad^2/s^2).
     *
     * @return estimated x coordinate variance of gyroscope bias or null if not available.
     */
    public Double getEstimatedGyroscopeBiasXVariance() {
        return estimatedGyroscopeCovariance != null
                ? estimatedGyroscopeCovariance.getElementAt(0, 0) : null;
    }

    /**
     * Gets estimated y coordinate variance of gyroscope bias expressed in (rad^2/s^2).
     *
     * @return estimated y coordinate variance of gyroscope bias or null if not available.
     */
    public Double getEstimatedGyroscopeBiasYVariance() {
        return estimatedGyroscopeCovariance != null
                ? estimatedGyroscopeCovariance.getElementAt(1, 1) : null;
    }

    /**
     * Gets estimated z coordinate variance of gyroscope bias expressed in (rad^2/s^2).
     *
     * @return estimated z coordinate variance of gyroscope bias or null if not available.
     */
    public Double getEstimatedGyroscopeBiasZVariance() {
        return estimatedGyroscopeCovariance != null
                ? estimatedGyroscopeCovariance.getElementAt(2, 2) : null;
    }

    /**
     * Gets the array containing x,y,z components of estimated gyroscope biases
     * expressed in radians per second (rad/s).
     *
     * @return array containing x,y,z components of estimated gyroscope biases.
     */
    public double[] getEstimatedGyroscopeBiases() {
        return estimatedGyroscopeBiases;
    }

    /**
     * Gets estimated gyroscope scale factors and cross-coupling errors.
     * This is the product of matrix Tg containing cross-coupling errors and Kg
     * containing scaling factors.
     * So that:
     * <pre>
     *     Mg = [sx    mxy  mxz] = Tg*Kg
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Kg = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Tg = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Mg = [sx    mxy  mxz] = Tg*Kg =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the gyroscope z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mg matrix
     * becomes upper diagonal:
     * <pre>
     *     Mg = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     *
     * @return estimated gyroscope scale factors and cross-coupling errors.
     */
    public Matrix getEstimatedGyroscopeMg() {
        return estimatedGyroscopeMg;
    }

    /**
     * Gets estimated G-dependent cross-biases introduced on the gyroscope by the
     * specific forces sensed by the accelerometer.
     *
     * @return a 3x3 matrix containing g-dependent cross biases.
     */
    public Matrix getEstimatedGyroscopeGg() {
        return estimatedGyroscopeGg;
    }

    /**
     * Gets the array containing x,y,z components of estimated magnetometer
     * hard-iron biases expressed in Teslas (T).
     *
     * @return array containing x,y,z components of estimated magnetometer
     * hard-iron biases.
     */
    public double[] getEstimatedMagnetometerHardIron() {
        return estimatedMagnetometerHardIron;
    }

    /**
     * Gets estimated magnetometer soft-iron matrix containing scale factors
     * and cross-coupling errors.
     * This is the product of matrix Tm containing cross-coupling errors and Km
     * containing scaling factors.
     * So that:
     * <pre>
     *     Mm = [sx    mxy  mxz] = Tm*Km
     *          [myx   sy   myz]
     *          [mzx   mzy  sz ]
     * </pre>
     * Where:
     * <pre>
     *     Km = [sx 0   0 ]
     *          [0  sy  0 ]
     *          [0  0   sz]
     * </pre>
     * and
     * <pre>
     *     Tm = [1          -alphaXy    alphaXz ]
     *          [alphaYx    1           -alphaYz]
     *          [-alphaZx   alphaZy     1       ]
     * </pre>
     * Hence:
     * <pre>
     *     Mm = [sx    mxy  mxz] = Tm*Km =  [sx             -sy * alphaXy   sz * alphaXz ]
     *          [myx   sy   myz]            [sx * alphaYx   sy              -sz * alphaYz]
     *          [mzx   mzy  sz ]            [-sx * alphaZx  sy * alphaZy    sz           ]
     * </pre>
     * This instance allows any 3x3 matrix. However, typically alphaYx, alphaZx and alphaZy
     * are considered to be zero if the accelerometer z-axis is assumed to be the same
     * as the body z-axis. When this is assumed, myx = mzx = mzy = 0 and the Mm matrix
     * becomes upper diagonal:
     * <pre>
     *     Mm = [sx    mxy  mxz]
     *          [0     sy   myz]
     *          [0     0    sz ]
     * </pre>
     * Values of this matrix are unit-less.
     *
     * @return estimated magnetometer soft-iron scale factors and cross-coupling errors,
     * or null if not available.
     */
    public Matrix getEstimatedMagnetometerMm() {
        return estimatedMagnetometerMm;
    }

    /**
     * Evaluates calibration Mean Square Error (MSE) for the provided threshold factor.
     *
     * @param thresholdFactor threshold factor to be used for interval detection
     *                        and measurement generation to be used for
     *                        calibration.
     * @return calibration MSE.
     * @throws LockedException                                   if the generator is busy.
     * @throws CalibrationException                              if calibration fails.
     * @throws NotReadyException                                 if the calibrator is not ready.
     * @throws IntervalDetectorThresholdFactorOptimizerException interval detection failed.
     */
    protected double evaluateForThresholdFactor(final double thresholdFactor) throws LockedException,
            CalibrationException, NotReadyException, IntervalDetectorThresholdFactorOptimizerException {
        if (accelerometerMeasurements == null) {
            accelerometerMeasurements = new ArrayList<>();
        } else {
            accelerometerMeasurements.clear();
        }

        if (gyroscopeSequences == null) {
            gyroscopeSequences = new ArrayList<>();
        } else {
            gyroscopeSequences.clear();
        }

        if (magnetometerMeasurements == null) {
            magnetometerMeasurements = new ArrayList<>();
        } else {
            magnetometerMeasurements.clear();
        }

        generator.reset();
        generator.setThresholdFactor(thresholdFactor);

        var count = dataSource.count();
        var failed = false;
        for (var i = 0; i < count; i++) {
            final var timedBodyKinematics = dataSource.getAt(i);
            if (!generator.process(timedBodyKinematics)) {
                failed = true;
                break;
            }
        }

        if (failed || generator.getStatus() == TriadStaticIntervalDetector.Status.FAILED) {
            // interval detection failed
            return Double.MAX_VALUE;
        }

        // check that enough measurements have been obtained
        if (accelerometerMeasurements.size() < accelerometerCalibrator.getMinimumRequiredMeasurements()
                || gyroscopeSequences.size() < gyroscopeCalibrator.getMinimumRequiredMeasurementsOrSequences()
                || magnetometerMeasurements.size() < magnetometerCalibrator.getMinimumRequiredMeasurements()) {
            return Double.MAX_VALUE;
        }

        // set calibrator measurements
        switch (accelerometerCalibrator.getMeasurementType()) {
            case STANDARD_DEVIATION_BODY_KINEMATICS:
                if (accelerometerCalibrator.isOrderedMeasurementsRequired()) {
                    final var calibrator =
                            (OrderedStandardDeviationBodyKinematicsAccelerometerCalibrator) accelerometerCalibrator;

                    calibrator.setMeasurements(accelerometerMeasurements);

                } else {
                    final var calibrator =
                            (UnorderedStandardDeviationBodyKinematicsAccelerometerCalibrator) accelerometerCalibrator;

                    calibrator.setMeasurements(accelerometerMeasurements);
                }

                if (accelerometerCalibrator.isQualityScoresRequired()) {
                    final var calibrator = (QualityScoredAccelerometerCalibrator) accelerometerCalibrator;

                    final var size = accelerometerMeasurements.size();
                    final var qualityScores = new double[size];
                    for (var i = 0; i < size; i++) {
                        qualityScores[i] = accelerometerQualityScoreMapper.map(accelerometerMeasurements.get(i));
                    }
                    calibrator.setQualityScores(qualityScores);
                }
                break;
            case FRAME_BODY_KINEMATICS, STANDARD_DEVIATION_FRAME_BODY_KINEMATICS:
                // Throw exception. Cannot use frames
            default:
                throw new IntervalDetectorThresholdFactorOptimizerException();
        }

        if (gyroscopeCalibrator.getMeasurementOrSequenceType()
                == GyroscopeCalibratorMeasurementOrSequenceType.BODY_KINEMATICS_SEQUENCE) {
            final var calibrator = (OrderedBodyKinematicsSequenceGyroscopeCalibrator) gyroscopeCalibrator;

            calibrator.setSequences(gyroscopeSequences);
        } else {
            throw new IntervalDetectorThresholdFactorOptimizerException();
        }

        if (gyroscopeCalibrator.isQualityScoresRequired()) {
            final var calibrator = (QualityScoredGyroscopeCalibrator) gyroscopeCalibrator;

            final var size = gyroscopeSequences.size();
            final var qualityScores = new double[size];
            for (var i = 0; i < size; i++) {
                qualityScores[i] = gyroscopeQualityScoreMapper.map(gyroscopeSequences.get(i));
            }
            calibrator.setQualityScores(qualityScores);
        }

        switch (magnetometerCalibrator.getMeasurementType()) {
            case STANDARD_DEVIATION_BODY_MAGNETIC_FLUX_DENSITY:
                if (magnetometerCalibrator.isOrderedMeasurementsRequired()) {
                    final var calibrator = (OrderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator)
                            magnetometerCalibrator;

                    calibrator.setMeasurements(magnetometerMeasurements);
                } else {
                    final var calibrator = (UnorderedStandardDeviationBodyMagneticFluxDensityMagnetometerCalibrator)
                            magnetometerCalibrator;

                    calibrator.setMeasurements(magnetometerMeasurements);
                }

                if (magnetometerCalibrator.isQualityScoresRequired()) {
                    final var calibrator = (QualityScoredMagnetometerCalibrator) magnetometerCalibrator;

                    final var size = magnetometerMeasurements.size();
                    final var qualityScores = new double[size];
                    for (var i = 0; i < size; i++) {
                        qualityScores[i] = magnetometerQualityScoreMapper.map(magnetometerMeasurements.get(i));
                    }
                    calibrator.setQualityScores(qualityScores);
                }
                break;
            case FRAME_BODY_MAGNETIC_FLUX_DENSITY, STANDARD_DEVIATION_FRAME_BODY_MAGNETIC_FLUX_DENSITY:
                // Throw exception. Cannot use frames
            default:
                throw new IntervalDetectorThresholdFactorOptimizerException();
        }

        accelerometerCalibrator.calibrate();

        // once we have accelerometer estimations, we can set known
        // accelerometer bias and cross-coupling errors to the gyroscope calibrator
        if (gyroscopeCalibrator instanceof AccelerometerDependentGyroscopeCalibrator accelGyroCalibrator) {

            final double[] bias;
            if (accelerometerCalibrator instanceof UnknownBiasAccelerometerCalibrator unknownBiasAccelerometerCalibrator) {
                bias = unknownBiasAccelerometerCalibrator.getEstimatedBiases();

            } else if (accelerometerCalibrator instanceof KnownBiasAccelerometerCalibrator knownBiasAccelerometerCalibrator) {
                bias = knownBiasAccelerometerCalibrator.getBias();
            } else {
                bias = null;
            }

            if (bias != null) {
                accelGyroCalibrator.setAccelerometerBias(bias);
                accelGyroCalibrator.setAccelerometerMa(accelerometerCalibrator.getEstimatedMa());
            }
        }

        gyroscopeCalibrator.calibrate();
        magnetometerCalibrator.calibrate();

        final var accelerometerMse = accelerometerCalibrator.getEstimatedMse();
        final var gyroscopeMse = gyroscopeCalibrator.getEstimatedMse();
        final var magnetometerMse = magnetometerCalibrator.getEstimatedMse();

        // convert accelerometer and gyroscope mse to global mse
        final var mse = mseRule.evaluate(accelerometerMse, gyroscopeMse, magnetometerMse);
        if (mse < minMse) {
            keepBestResult(mse, thresholdFactor);
        }
        return mse;
    }

    /**
     * Initializes accelerometer, gyroscope and magnetometer measurement generator to
     * convert {@link TimedBodyKinematicsAndMagneticFluxDensity} measurements after
     * interval detection into measurements and sequences used for accelerometer,
     * gyroscope and magnetometer calibration.
     */
    private void initialize() {
        final var generatorListener = new AccelerometerGyroscopeAndMagnetometerMeasurementsGeneratorListener() {
            @Override
            public void onInitializationStarted(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }

            @Override
            public void onInitializationCompleted(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator,
                    final double accelerometerBaseNoiseLevel) {
                // not needed
            }

            @Override
            public void onError(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator,
                    final TriadStaticIntervalDetector.ErrorReason reason) {
                // not needed
            }

            @Override
            public void onStaticIntervalDetected(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }

            @Override
            public void onDynamicIntervalDetected(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }

            @Override
            public void onStaticIntervalSkipped(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }

            @Override
            public void onDynamicIntervalSkipped(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }

            @Override
            public void onGeneratedAccelerometerMeasurement(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator,
                    final StandardDeviationBodyKinematics measurement) {
                accelerometerMeasurements.add(measurement);
            }

            @Override
            public void onGeneratedGyroscopeMeasurement(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator,
                    final BodyKinematicsSequence<StandardDeviationTimedBodyKinematics> sequence) {
                gyroscopeSequences.add(sequence);
            }

            @Override
            public void onGeneratedMagnetometerMeasurement(
                    final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator,
                    final StandardDeviationBodyMagneticFluxDensity measurement) {
                magnetometerMeasurements.add(measurement);
            }

            @Override
            public void onReset(final AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator generator) {
                // not needed
            }
        };

        generator = new AccelerometerGyroscopeAndMagnetometerMeasurementsGenerator(generatorListener);
    }

    /**
     * Keeps the best calibration solution found so far.
     *
     * @param mse             Estimated Mean Square Error during calibration.
     * @param thresholdFactor threshold factor to be kept.
     */
    private void keepBestResult(final double mse, final double thresholdFactor) {
        minMse = mse;
        optimalThresholdFactor = thresholdFactor;

        angularSpeedNoiseRootPsd = generator.getGyroscopeBaseNoiseLevelRootPsd();
        baseNoiseLevel = generator.getAccelerometerBaseNoiseLevel();
        threshold = generator.getThreshold();

        if (estimatedAccelerometerCovariance == null) {
            estimatedAccelerometerCovariance = new Matrix(accelerometerCalibrator.getEstimatedCovariance());
        } else {
            estimatedAccelerometerCovariance.copyFrom(accelerometerCalibrator.getEstimatedCovariance());
        }
        if (estimatedAccelerometerMa == null) {
            estimatedAccelerometerMa = new Matrix(accelerometerCalibrator.getEstimatedMa());
        } else {
            estimatedAccelerometerMa.copyFrom(accelerometerCalibrator.getEstimatedMa());
        }
        if (accelerometerCalibrator instanceof UnknownBiasAccelerometerCalibrator unknownBiasAccelerometerCalibrator) {
            estimatedAccelerometerBiases = unknownBiasAccelerometerCalibrator.getEstimatedBiases();
        } else if (accelerometerCalibrator instanceof KnownBiasAccelerometerCalibrator knownBiasAccelerometerCalibrator) {
            estimatedAccelerometerBiases = knownBiasAccelerometerCalibrator.getBias();
        }

        if (estimatedGyroscopeCovariance == null) {
            estimatedGyroscopeCovariance = new Matrix(gyroscopeCalibrator.getEstimatedCovariance());
        } else {
            estimatedGyroscopeCovariance.copyFrom(gyroscopeCalibrator.getEstimatedCovariance());
        }
        if (estimatedGyroscopeMg == null) {
            estimatedGyroscopeMg = new Matrix(gyroscopeCalibrator.getEstimatedMg());
        } else {
            estimatedGyroscopeMg.copyFrom(gyroscopeCalibrator.getEstimatedMg());
        }
        if (estimatedGyroscopeGg == null) {
            estimatedGyroscopeGg = new Matrix(gyroscopeCalibrator.getEstimatedGg());
        } else {
            estimatedGyroscopeGg.copyFrom(gyroscopeCalibrator.getEstimatedGg());
        }
        if (gyroscopeCalibrator instanceof UnknownBiasGyroscopeCalibrator unknownBiasGyroscopeCalibrator) {
            estimatedGyroscopeBiases = unknownBiasGyroscopeCalibrator.getEstimatedBiases();
        } else if (gyroscopeCalibrator instanceof KnownBiasAccelerometerCalibrator knownBiasAccelerometerCalibrator) {
            estimatedGyroscopeBiases = knownBiasAccelerometerCalibrator.getBias();
        }

        if (estimatedMagnetometerCovariance == null) {
            estimatedMagnetometerCovariance = new Matrix(magnetometerCalibrator.getEstimatedCovariance());
        } else {
            estimatedMagnetometerCovariance.copyFrom(magnetometerCalibrator.getEstimatedCovariance());
        }
        if (estimatedMagnetometerMm == null) {
            estimatedMagnetometerMm = new Matrix(magnetometerCalibrator.getEstimatedMm());
        } else {
            estimatedMagnetometerMm.copyFrom(magnetometerCalibrator.getEstimatedMm());
        }
        if (magnetometerCalibrator instanceof UnknownHardIronMagnetometerCalibrator unknownHardIronMagnetometerCalibrator) {
            estimatedMagnetometerHardIron = unknownHardIronMagnetometerCalibrator.getEstimatedHardIron();
        } else if (magnetometerCalibrator instanceof KnownHardIronMagnetometerCalibrator knownHardIronMagnetometerCalibrator) {
            estimatedMagnetometerHardIron = knownHardIronMagnetometerCalibrator.getHardIron();
        }
    }

    /**
     * Creates an acceleration instance using the provided value and unit.
     *
     * @param value value of measurement.
     * @param unit  unit of value.
     * @return created acceleration.
     */
    private Acceleration createMeasurement(final double value, final AccelerationUnit unit) {
        return new Acceleration(value, unit);
    }

    /**
     * Gets the default unit for acceleration, which is meters per
     * squared second (m/s^2).
     *
     * @return default unit for acceleration.
     */
    private AccelerationUnit getDefaultUnit() {
        return AccelerationUnit.METERS_PER_SQUARED_SECOND;
    }
}