SemiAnalyticalKalmanModel.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You 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 org.orekit.estimation.sequential;

import org.hipparchus.exception.MathRuntimeException;
import org.hipparchus.filtering.kalman.ProcessEstimate;
import org.hipparchus.filtering.kalman.extended.ExtendedKalmanFilter;
import org.hipparchus.filtering.kalman.extended.NonLinearEvolution;
import org.hipparchus.filtering.kalman.extended.NonLinearProcess;
import org.hipparchus.linear.Array2DRowRealMatrix;
import org.hipparchus.linear.ArrayRealVector;
import org.hipparchus.linear.MatrixUtils;
import org.hipparchus.linear.QRDecomposition;
import org.hipparchus.linear.RealMatrix;
import org.hipparchus.linear.RealVector;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.estimation.measurements.EstimatedMeasurement;
import org.orekit.estimation.measurements.ObservedMeasurement;
import org.orekit.orbits.Orbit;
import org.orekit.orbits.OrbitType;
import org.orekit.propagation.PropagationType;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.conversion.DSSTPropagatorBuilder;
import org.orekit.propagation.semianalytical.dsst.DSSTHarvester;
import org.orekit.propagation.semianalytical.dsst.DSSTPropagator;
import org.orekit.propagation.semianalytical.dsst.forces.DSSTForceModel;
import org.orekit.propagation.semianalytical.dsst.forces.ShortPeriodTerms;
import org.orekit.propagation.semianalytical.dsst.utilities.AuxiliaryElements;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.ChronologicalComparator;
import org.orekit.utils.ParameterDriver;
import org.orekit.utils.ParameterDriversList;
import org.orekit.utils.ParameterDriversList.DelegatingDriver;
import org.orekit.utils.TimeSpanMap.Span;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Process model to use with a {@link SemiAnalyticalKalmanEstimator}.
 *
 * @see "Folcik Z., Orbit Determination Using Modern Filters/Smoothers and Continuous Thrust Modeling,
 *       Master of Science Thesis, Department of Aeronautics and Astronautics, MIT, June, 2008."
 *
 * @see "Cazabonne B., Bayard J., Journot M., and Cefola P. J., A Semi-analytical Approach for Orbit
 *       Determination based on Extended Kalman Filter, AAS Paper 21-614, AAS/AIAA Astrodynamics
 *       Specialist Conference, Big Sky, August 2021."
 *
 * @author Julie Bayard
 * @author Bryan Cazabonne
 * @author Maxime Journot
 * @since 11.1
 */
public  class SemiAnalyticalKalmanModel implements KalmanEstimation, NonLinearProcess<MeasurementDecorator>, SemiAnalyticalProcess {

    /** Builders for DSST propagator. */
    private final DSSTPropagatorBuilder builder;

    /** Estimated orbital parameters. */
    private final ParameterDriversList estimatedOrbitalParameters;

    /** Per-builder estimated propagation drivers. */
    private final ParameterDriversList estimatedPropagationParameters;

    /** Estimated measurements parameters. */
    private final ParameterDriversList estimatedMeasurementsParameters;

    /** Map for propagation parameters columns. */
    private final Map<String, Integer> propagationParameterColumns;

    /** Map for measurements parameters columns. */
    private final Map<String, Integer> measurementParameterColumns;

    /** Scaling factors. */
    private final double[] scale;

    /** Provider for covariance matrix. */
    private final CovarianceMatrixProvider covarianceMatrixProvider;

    /** Process noise matrix provider for measurement parameters. */
    private final CovarianceMatrixProvider measurementProcessNoiseMatrix;

    /** Harvester between two-dimensional Jacobian matrices and one-dimensional additional state arrays. */
    private DSSTHarvester harvester;

    /** Propagators for the reference trajectories, up to current date. */
    private DSSTPropagator dsstPropagator;

    /** Observer to retrieve current estimation info. */
    private KalmanObserver observer;

    /** Current number of measurement. */
    private int currentMeasurementNumber;

    /** Current date. */
    private AbsoluteDate currentDate;

    /** Predicted mean element filter correction. */
    private RealVector predictedFilterCorrection;

    /** Corrected mean element filter correction. */
    private RealVector correctedFilterCorrection;

    /** Predicted measurement. */
    private EstimatedMeasurement<?> predictedMeasurement;

    /** Corrected measurement. */
    private EstimatedMeasurement<?> correctedMeasurement;

    /** Nominal mean spacecraft state. */
    private SpacecraftState nominalMeanSpacecraftState;

    /** Previous nominal mean spacecraft state. */
    private SpacecraftState previousNominalMeanSpacecraftState;

    /** Current corrected estimate. */
    private ProcessEstimate correctedEstimate;

    /** Inverse of the orbital part of the state transition matrix. */
    private RealMatrix phiS;

    /** Propagation parameters part of the state transition matrix. */
    private RealMatrix psiS;

    /** Kalman process model constructor (package private).
     * @param propagatorBuilder propagators builders used to evaluate the orbits.
     * @param covarianceMatrixProvider provider for covariance matrix
     * @param estimatedMeasurementParameters measurement parameters to estimate
     * @param measurementProcessNoiseMatrix provider for measurement process noise matrix
     */
    protected SemiAnalyticalKalmanModel(final DSSTPropagatorBuilder propagatorBuilder,
                                        final CovarianceMatrixProvider covarianceMatrixProvider,
                                        final ParameterDriversList estimatedMeasurementParameters,
                                        final CovarianceMatrixProvider measurementProcessNoiseMatrix) {

        this.builder                         = propagatorBuilder;
        this.estimatedMeasurementsParameters = estimatedMeasurementParameters;
        this.measurementParameterColumns     = new HashMap<>(estimatedMeasurementsParameters.getDrivers().size());
        this.observer                        = null;
        this.currentMeasurementNumber        = 0;
        this.currentDate                     = propagatorBuilder.getInitialOrbitDate();
        this.covarianceMatrixProvider        = covarianceMatrixProvider;
        this.measurementProcessNoiseMatrix   = measurementProcessNoiseMatrix;

        // Number of estimated parameters
        int columns = 0;

        // Set estimated orbital parameters
        estimatedOrbitalParameters = new ParameterDriversList();
        for (final ParameterDriver driver : builder.getOrbitalParametersDrivers().getDrivers()) {

            // Verify if the driver reference date has been set
            if (driver.getReferenceDate() == null) {
                driver.setReferenceDate(currentDate);
            }

            // Verify if the driver is selected
            if (driver.isSelected()) {
                estimatedOrbitalParameters.add(driver);
                columns++;
            }

        }

        // Set estimated propagation parameters
        estimatedPropagationParameters = new ParameterDriversList();
        final List<String> estimatedPropagationParametersNames = new ArrayList<>();
        for (final ParameterDriver driver : builder.getPropagationParametersDrivers().getDrivers()) {

            // Verify if the driver reference date has been set
            if (driver.getReferenceDate() == null) {
                driver.setReferenceDate(currentDate);
            }

            // Verify if the driver is selected
            if (driver.isSelected()) {
                estimatedPropagationParameters.add(driver);
                // Add the driver name if it has not been added yet
                for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {

                    if (!estimatedPropagationParametersNames.contains(span.getData())) {
                        estimatedPropagationParametersNames.add(span.getData());
                    }
                }
            }

        }
        estimatedPropagationParametersNames.sort(Comparator.naturalOrder());

        // Populate the map of propagation drivers' columns and update the total number of columns
        propagationParameterColumns = new HashMap<>(estimatedPropagationParametersNames.size());
        for (final String driverName : estimatedPropagationParametersNames) {
            propagationParameterColumns.put(driverName, columns);
            ++columns;
        }

        // Set the estimated measurement parameters
        for (final ParameterDriver parameter : estimatedMeasurementsParameters.getDrivers()) {
            if (parameter.getReferenceDate() == null) {
                parameter.setReferenceDate(currentDate);
            }
            for (Span<String> span = parameter.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
                measurementParameterColumns.put(span.getData(), columns);
                ++columns;
            }
        }

        // Compute the scale factors
        this.scale = new double[columns];
        int index = 0;
        for (final ParameterDriver driver : estimatedOrbitalParameters.getDrivers()) {
            scale[index++] = driver.getScale();
        }
        for (final ParameterDriver driver : estimatedPropagationParameters.getDrivers()) {
            for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
                scale[index++] = driver.getScale();
            }
        }
        for (final ParameterDriver driver : estimatedMeasurementsParameters.getDrivers()) {
            for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
                scale[index++] = driver.getScale();
            }
        }

        // Build the reference propagator and add its partial derivatives equations implementation
        updateReferenceTrajectory(getEstimatedPropagator());
        this.nominalMeanSpacecraftState = dsstPropagator.getInitialState();
        this.previousNominalMeanSpacecraftState = nominalMeanSpacecraftState;

        // Initialize "field" short periodic terms
        harvester.initializeFieldShortPeriodTerms(nominalMeanSpacecraftState);

        // Initialize the estimated normalized mean element filter correction (See Ref [1], Eq. 3.2a)
        this.predictedFilterCorrection = MatrixUtils.createRealVector(columns);
        this.correctedFilterCorrection = predictedFilterCorrection;

        // Initialize propagation parameters part of the state transition matrix (See Ref [1], Eq. 3.2c)
        this.psiS = null;
        if (estimatedPropagationParameters.getNbParams() != 0) {
            this.psiS = MatrixUtils.createRealMatrix(getNumberSelectedOrbitalDriversValuesToEstimate(),
                                                     getNumberSelectedPropagationDriversValuesToEstimate());
        }

        // Initialize inverse of the orbital part of the state transition matrix (See Ref [1], Eq. 3.2d)
        this.phiS = MatrixUtils.createRealIdentityMatrix(getNumberSelectedOrbitalDriversValuesToEstimate());

        // Number of estimated measurement parameters
        final int nbMeas = getNumberSelectedMeasurementDriversValuesToEstimate();

        // Number of estimated dynamic parameters (orbital + propagation)
        final int nbDyn  = getNumberSelectedOrbitalDriversValuesToEstimate() + getNumberSelectedPropagationDriversValuesToEstimate();

        // Covariance matrix
        final RealMatrix noiseK = MatrixUtils.createRealMatrix(nbDyn + nbMeas, nbDyn + nbMeas);
        final RealMatrix noiseP = covarianceMatrixProvider.getInitialCovarianceMatrix(nominalMeanSpacecraftState);
        noiseK.setSubMatrix(noiseP.getData(), 0, 0);
        if (measurementProcessNoiseMatrix != null) {
            final RealMatrix noiseM = measurementProcessNoiseMatrix.getInitialCovarianceMatrix(nominalMeanSpacecraftState);
            noiseK.setSubMatrix(noiseM.getData(), nbDyn, nbDyn);
        }

        // Verify dimension
        KalmanEstimatorUtil.checkDimension(noiseK.getRowDimension(),
                                           builder.getOrbitalParametersDrivers(),
                                           builder.getPropagationParametersDrivers(),
                                           estimatedMeasurementsParameters);

        final RealMatrix correctedCovariance = KalmanEstimatorUtil.normalizeCovarianceMatrix(noiseK, scale);

        // Initialize corrected estimate
        this.correctedEstimate = new ProcessEstimate(0.0, correctedFilterCorrection, correctedCovariance);

    }

    /** {@inheritDoc} */
    @Override
    public KalmanObserver getObserver() {
        return observer;
    }

    /** Set the observer.
     * @param observer the observer
     */
    public void setObserver(final KalmanObserver observer) {
        this.observer = observer;
    }

    /** Get the current corrected estimate.
     * @return current corrected estimate
     */
    public ProcessEstimate getEstimate() {
        return correctedEstimate;
    }

    /** Process a single measurement.
     * <p>
     * Update the filter with the new measurements.
     * </p>
     * @param observedMeasurements the list of measurements to process
     * @param filter Extended Kalman Filter
     * @return estimated propagator
     */
    public DSSTPropagator processMeasurements(final List<ObservedMeasurement<?>> observedMeasurements,
                                              final ExtendedKalmanFilter<MeasurementDecorator> filter) {
        try {

            // Sort the measurement
            observedMeasurements.sort(new ChronologicalComparator());
            final AbsoluteDate tStart             = observedMeasurements.get(0).getDate();
            final AbsoluteDate tEnd               = observedMeasurements.get(observedMeasurements.size() - 1).getDate();
            final double       overshootTimeRange = FastMath.nextAfter(tEnd.durationFrom(tStart),
                                                    Double.POSITIVE_INFINITY);

            // Initialize step handler and set it to the propagator
            final SemiAnalyticalMeasurementHandler stepHandler = new SemiAnalyticalMeasurementHandler(this, filter, observedMeasurements, builder.getInitialOrbitDate());
            dsstPropagator.getMultiplexer().add(stepHandler);
            dsstPropagator.propagate(tStart, tStart.shiftedBy(overshootTimeRange));

            // Return the last estimated propagator
            return getEstimatedPropagator();

        } catch (MathRuntimeException mrte) {
            throw new OrekitException(mrte);
        }
    }

    /** Get the propagator estimated with the values set in the propagator builder.
     * @return propagator based on the current values in the builder
     */
    public DSSTPropagator getEstimatedPropagator() {
        // Return propagator built with current instantiation of the propagator builder
        return (DSSTPropagator) builder.buildPropagator();
    }

    /** {@inheritDoc} */
    @Override
    public NonLinearEvolution getEvolution(final double previousTime, final RealVector previousState,
                                           final MeasurementDecorator measurement) {

        // Set a reference date for all measurements parameters that lack one (including the not estimated ones)
        final ObservedMeasurement<?> observedMeasurement = measurement.getObservedMeasurement();
        for (final ParameterDriver driver : observedMeasurement.getParametersDrivers()) {
            if (driver.getReferenceDate() == null) {
                driver.setReferenceDate(builder.getInitialOrbitDate());
            }
        }

        // Increment measurement number
        ++currentMeasurementNumber;

        // Update the current date
        currentDate = measurement.getObservedMeasurement().getDate();

        // Normalized state transition matrix
        final RealMatrix stm = getErrorStateTransitionMatrix();

        // Predict filter correction
        predictedFilterCorrection = predictFilterCorrection(stm);

        // Short period term derivatives
        analyticalDerivativeComputations(nominalMeanSpacecraftState);

        // Calculate the predicted osculating elements
        final double[] osculating = computeOsculatingElements(predictedFilterCorrection);
        final Orbit osculatingOrbit = OrbitType.EQUINOCTIAL.mapArrayToOrbit(osculating, null, builder.getPositionAngleType(),
                                                                            currentDate, nominalMeanSpacecraftState.getMu(),
                                                                            nominalMeanSpacecraftState.getFrame());

        // Compute the predicted measurements  (See Ref [1], Eq. 3.8)
        predictedMeasurement = observedMeasurement.estimate(currentMeasurementNumber,
                                                            currentMeasurementNumber,
                                                            new SpacecraftState[] {
                                                                new SpacecraftState(osculatingOrbit,
                                                                                    nominalMeanSpacecraftState.getAttitude(),
                                                                                    nominalMeanSpacecraftState.getMass(),
                                                                                    nominalMeanSpacecraftState.getAdditionalStatesValues(),
                                                                                    nominalMeanSpacecraftState.getAdditionalStatesDerivatives())
                                                            });

        // Normalized measurement matrix
        final RealMatrix measurementMatrix = getMeasurementMatrix();

        // Number of estimated measurement parameters
        final int nbMeas = getNumberSelectedMeasurementDriversValuesToEstimate();

        // Number of estimated dynamic parameters (orbital + propagation)
        final int nbDyn  = getNumberSelectedOrbitalDriversValuesToEstimate() + getNumberSelectedPropagationDriversValuesToEstimate();

        // Covariance matrix
        final RealMatrix noiseK = MatrixUtils.createRealMatrix(nbDyn + nbMeas, nbDyn + nbMeas);
        final RealMatrix noiseP = covarianceMatrixProvider.getProcessNoiseMatrix(previousNominalMeanSpacecraftState, nominalMeanSpacecraftState);
        noiseK.setSubMatrix(noiseP.getData(), 0, 0);
        if (measurementProcessNoiseMatrix != null) {
            final RealMatrix noiseM = measurementProcessNoiseMatrix.getProcessNoiseMatrix(previousNominalMeanSpacecraftState, nominalMeanSpacecraftState);
            noiseK.setSubMatrix(noiseM.getData(), nbDyn, nbDyn);
        }

        // Verify dimension
        KalmanEstimatorUtil.checkDimension(noiseK.getRowDimension(),
                                           builder.getOrbitalParametersDrivers(),
                                           builder.getPropagationParametersDrivers(),
                                           estimatedMeasurementsParameters);

        final RealMatrix normalizedProcessNoise = KalmanEstimatorUtil.normalizeCovarianceMatrix(noiseK, scale);

        // Return
        return new NonLinearEvolution(measurement.getTime(), predictedFilterCorrection, stm,
                                      normalizedProcessNoise, measurementMatrix);
    }

    /** {@inheritDoc} */
    @Override
    public RealVector getInnovation(final MeasurementDecorator measurement, final NonLinearEvolution evolution,
                                    final RealMatrix innovationCovarianceMatrix) {

        // Apply the dynamic outlier filter, if it exists
        KalmanEstimatorUtil.applyDynamicOutlierFilter(predictedMeasurement, innovationCovarianceMatrix);
        // Compute the innovation vector
        return KalmanEstimatorUtil.computeInnovationVector(predictedMeasurement, predictedMeasurement.getObservedMeasurement().getTheoreticalStandardDeviation());
    }

    /** {@inheritDoc} */
    @Override
    public void finalizeEstimation(final ObservedMeasurement<?> observedMeasurement,
                                   final ProcessEstimate estimate) {
        // Update the process estimate
        correctedEstimate = estimate;
        // Corrected filter correction
        correctedFilterCorrection = estimate.getState();
        // Update the previous nominal mean spacecraft state
        previousNominalMeanSpacecraftState = nominalMeanSpacecraftState;
        // Calculate the corrected osculating elements
        final double[] osculating = computeOsculatingElements(correctedFilterCorrection);
        final Orbit osculatingOrbit = OrbitType.EQUINOCTIAL.mapArrayToOrbit(osculating, null, builder.getPositionAngleType(),
                                                                            currentDate, nominalMeanSpacecraftState.getMu(),
                                                                            nominalMeanSpacecraftState.getFrame());

        // Compute the corrected measurements
        correctedMeasurement = observedMeasurement.estimate(currentMeasurementNumber,
                                                            currentMeasurementNumber,
                                                            new SpacecraftState[] {
                                                                new SpacecraftState(osculatingOrbit,
                                                                                    nominalMeanSpacecraftState.getAttitude(),
                                                                                    nominalMeanSpacecraftState.getMass(),
                                                                                    nominalMeanSpacecraftState.getAdditionalStatesValues(),
                                                                                    nominalMeanSpacecraftState.getAdditionalStatesDerivatives())
                                                            });
        // Call the observer if the user add one
        if (observer != null) {
            observer.evaluationPerformed(this);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void finalizeOperationsObservationGrid() {
        // Update parameters
        updateParameters();
    }

    /** {@inheritDoc} */
    @Override
    public ParameterDriversList getEstimatedOrbitalParameters() {
        return estimatedOrbitalParameters;
    }

    /** {@inheritDoc} */
    @Override
    public ParameterDriversList getEstimatedPropagationParameters() {
        return estimatedPropagationParameters;
    }

    /** {@inheritDoc} */
    @Override
    public ParameterDriversList getEstimatedMeasurementsParameters() {
        return estimatedMeasurementsParameters;
    }

    /** {@inheritDoc} */
    @Override
    public SpacecraftState[] getPredictedSpacecraftStates() {
        return new SpacecraftState[] {nominalMeanSpacecraftState};
    }

    /** {@inheritDoc} */
    @Override
    public SpacecraftState[] getCorrectedSpacecraftStates() {
        return new SpacecraftState[] {getEstimatedPropagator().getInitialState()};
    }

    /** {@inheritDoc} */
    @Override
    public RealVector getPhysicalEstimatedState() {
        // Method {@link ParameterDriver#getValue()} is used to get
        // the physical values of the state.
        // The scales'array is used to get the size of the state vector
        final RealVector physicalEstimatedState = new ArrayRealVector(scale.length);
        int i = 0;
        for (final DelegatingDriver driver : getEstimatedOrbitalParameters().getDrivers()) {
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                physicalEstimatedState.setEntry(i++, span.getData());
            }
        }
        for (final DelegatingDriver driver : getEstimatedPropagationParameters().getDrivers()) {
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                physicalEstimatedState.setEntry(i++, span.getData());
            }
        }
        for (final DelegatingDriver driver : getEstimatedMeasurementsParameters().getDrivers()) {
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                physicalEstimatedState.setEntry(i++, span.getData());
            }
        }

        return physicalEstimatedState;
    }

    /** {@inheritDoc} */
    @Override
    public RealMatrix getPhysicalEstimatedCovarianceMatrix() {
        // Un-normalize the estimated covariance matrix (P) from Hipparchus and return it.
        // The covariance P is an mxm matrix where m = nbOrb + nbPropag + nbMeas
        // For each element [i,j] of P the corresponding normalized value is:
        // Pn[i,j] = P[i,j] / (scale[i]*scale[j])
        // Consequently: P[i,j] = Pn[i,j] * scale[i] * scale[j]
        return KalmanEstimatorUtil.unnormalizeCovarianceMatrix(correctedEstimate.getCovariance(), scale);
    }

    /** {@inheritDoc} */
    @Override
    public RealMatrix getPhysicalStateTransitionMatrix() {
        //  Un-normalize the state transition matrix (φ) from Hipparchus and return it.
        // φ is an mxm matrix where m = nbOrb + nbPropag + nbMeas
        // For each element [i,j] of normalized φ (φn), the corresponding physical value is:
        // φ[i,j] = φn[i,j] * scale[i] / scale[j]
        return correctedEstimate.getStateTransitionMatrix() == null ?
                null : KalmanEstimatorUtil.unnormalizeStateTransitionMatrix(correctedEstimate.getStateTransitionMatrix(), scale);
    }

    /** {@inheritDoc} */
    @Override
    public RealMatrix getPhysicalMeasurementJacobian() {
        // Un-normalize the measurement matrix (H) from Hipparchus and return it.
        // H is an nxm matrix where:
        //  - m = nbOrb + nbPropag + nbMeas is the number of estimated parameters
        //  - n is the size of the measurement being processed by the filter
        // For each element [i,j] of normalized H (Hn) the corresponding physical value is:
        // H[i,j] = Hn[i,j] * σ[i] / scale[j]
        return correctedEstimate.getMeasurementJacobian() == null ?
                null : KalmanEstimatorUtil.unnormalizeMeasurementJacobian(correctedEstimate.getMeasurementJacobian(),
                                                                          scale,
                                                                          correctedMeasurement.getObservedMeasurement().getTheoreticalStandardDeviation());
    }

    /** {@inheritDoc} */
    @Override
    public RealMatrix getPhysicalInnovationCovarianceMatrix() {
        // Un-normalize the innovation covariance matrix (S) from Hipparchus and return it.
        // S is an nxn matrix where n is the size of the measurement being processed by the filter
        // For each element [i,j] of normalized S (Sn) the corresponding physical value is:
        // S[i,j] = Sn[i,j] * σ[i] * σ[j]
        return correctedEstimate.getInnovationCovariance() == null ?
                null : KalmanEstimatorUtil.unnormalizeInnovationCovarianceMatrix(correctedEstimate.getInnovationCovariance(),
                                                                                 predictedMeasurement.getObservedMeasurement().getTheoreticalStandardDeviation());
    }

    /** {@inheritDoc} */
    @Override
    public RealMatrix getPhysicalKalmanGain() {
        // Un-normalize the Kalman gain (K) from Hipparchus and return it.
        // K is an mxn matrix where:
        //  - m = nbOrb + nbPropag + nbMeas is the number of estimated parameters
        //  - n is the size of the measurement being processed by the filter
        // For each element [i,j] of normalized K (Kn) the corresponding physical value is:
        // K[i,j] = Kn[i,j] * scale[i] / σ[j]
        return correctedEstimate.getKalmanGain() == null ?
                null : KalmanEstimatorUtil.unnormalizeKalmanGainMatrix(correctedEstimate.getKalmanGain(),
                                                                       scale,
                                                                       correctedMeasurement.getObservedMeasurement().getTheoreticalStandardDeviation());
    }

    /** {@inheritDoc} */
    @Override
    public int getCurrentMeasurementNumber() {
        return currentMeasurementNumber;
    }

    /** {@inheritDoc} */
    @Override
    public AbsoluteDate getCurrentDate() {
        return currentDate;
    }

    /** {@inheritDoc} */
    @Override
    public EstimatedMeasurement<?> getPredictedMeasurement() {
        return predictedMeasurement;
    }

    /** {@inheritDoc} */
    @Override
    public EstimatedMeasurement<?> getCorrectedMeasurement() {
        return correctedMeasurement;
    }

    /** {@inheritDoc} */
    @Override
    public void updateNominalSpacecraftState(final SpacecraftState nominal) {
        this.nominalMeanSpacecraftState = nominal;
        // Update the builder with the nominal mean elements orbit
        builder.resetOrbit(nominal.getOrbit(), PropagationType.MEAN);

        // Additionally, update the builder with the predicted mass value.
        // If any mass changes have occurred during this estimation step, such as maneuvers,
        // the updated mass value must be carried over so that new Propagators from this builder start with the updated mass.
        builder.setMass(nominal.getMass());
    }

    /** Update the reference trajectories using the propagator as input.
     * @param propagator The new propagator to use
     */
    public void updateReferenceTrajectory(final DSSTPropagator propagator) {

        dsstPropagator = propagator;

        // Equation name
        final String equationName = SemiAnalyticalKalmanEstimator.class.getName() + "-derivatives-";

        // Mean state
        final SpacecraftState meanState = dsstPropagator.initialIsOsculating() ?
                       DSSTPropagator.computeMeanState(dsstPropagator.getInitialState(), dsstPropagator.getAttitudeProvider(), dsstPropagator.getAllForceModels()) :
                       dsstPropagator.getInitialState();

        // Update the jacobian harvester
        dsstPropagator.setInitialState(meanState, PropagationType.MEAN);
        harvester = dsstPropagator.setupMatricesComputation(equationName, null, null);

    }

    /** {@inheritDoc} */
    @Override
    public void updateShortPeriods(final SpacecraftState state) {
        // Loop on DSST force models
        for (final DSSTForceModel model : builder.getAllForceModels()) {
            model.updateShortPeriodTerms(model.getParametersAllValues(), state);
        }
        harvester.updateFieldShortPeriodTerms(state);
    }

    /** {@inheritDoc} */
    @Override
    public void initializeShortPeriodicTerms(final SpacecraftState meanState) {
        final List<ShortPeriodTerms> shortPeriodTerms = new ArrayList<>();
        // initialize ForceModels in OSCULATING mode even if propagation is MEAN
        final PropagationType type = PropagationType.OSCULATING;
        for (final DSSTForceModel force :  builder.getAllForceModels()) {
            shortPeriodTerms.addAll(force.initializeShortPeriodTerms(new AuxiliaryElements(meanState.getOrbit(), 1), type, force.getParameters(meanState.getDate())));
        }
        dsstPropagator.setShortPeriodTerms(shortPeriodTerms);
        // also need to initialize the Field terms in the same mode
        harvester.initializeFieldShortPeriodTerms(meanState, type);
    }

    /** Get the normalized state transition matrix (STM) from previous point to current point.
     * The STM contains the partial derivatives of current state with respect to previous state.
     * The  STM is an mxm matrix where m is the size of the state vector.
     * m = nbOrb + nbPropag + nbMeas
     * @return the normalized error state transition matrix
     */
    private RealMatrix getErrorStateTransitionMatrix() {

        /* The state transition matrix is obtained as follows, with:
         *  - Phi(k, k-1) : Transitional orbital matrix
         *  - Psi(k, k-1) : Transitional propagation parameters matrix
         *
         *       |             |             |   .    |
         *       | Phi(k, k-1) | Psi(k, k-1) | ..0..  |
         *       |             |             |   .    |
         *       |-------------|-------------|--------|
         *       |      .      |    1 0 0    |   .    |
         * STM = |    ..0..    |    0 1 0    | ..0..  |
         *       |      .      |    0 0 1    |   .    |
         *       |-------------|-------------|--------|
         *       |      .      |      .      | 1 0 0..|
         *       |    ..0..    |    ..0..    | 0 1 0..|
         *       |      .      |      .      | 0 0 1..|
         */

        // Initialize to the proper size identity matrix
        final RealMatrix stm = MatrixUtils.createRealIdentityMatrix(correctedEstimate.getState().getDimension());

        // Derivatives of the state vector with respect to initial state vector
        final int nbOrb = getNumberSelectedOrbitalDriversValuesToEstimate();
        final RealMatrix dYdY0 = harvester.getB2(nominalMeanSpacecraftState);

        // Calculate transitional orbital matrix (See Ref [1], Eq. 3.4a)
        final RealMatrix phi = dYdY0.multiply(phiS);

        // Fill the state transition matrix with the orbital drivers
        final List<DelegatingDriver> drivers = builder.getOrbitalParametersDrivers().getDrivers();
        for (int i = 0; i < nbOrb; ++i) {
            if (drivers.get(i).isSelected()) {
                int jOrb = 0;
                for (int j = 0; j < nbOrb; ++j) {
                    if (drivers.get(j).isSelected()) {
                        stm.setEntry(i, jOrb++, phi.getEntry(i, j));
                    }
                }
            }
        }

        // Update PhiS
        phiS = new QRDecomposition(dYdY0).getSolver().getInverse();

        // Derivatives of the state vector with respect to propagation parameters
        if (psiS != null) {

            final int nbProp = getNumberSelectedPropagationDriversValuesToEstimate();
            final RealMatrix dYdPp = harvester.getB3(nominalMeanSpacecraftState);

            // Calculate transitional parameters matrix (See Ref [1], Eq. 3.4b)
            final RealMatrix psi = dYdPp.subtract(phi.multiply(psiS));

            // Fill 1st row, 2nd column (dY/dPp)
            for (int i = 0; i < nbOrb; ++i) {
                for (int j = 0; j < nbProp; ++j) {
                    stm.setEntry(i, j + nbOrb, psi.getEntry(i, j));
                }
            }

            // Update PsiS
            psiS = dYdPp;

        }

        // Normalization of the STM
        // normalized(STM)ij = STMij*Sj/Si
        for (int i = 0; i < scale.length; i++) {
            for (int j = 0; j < scale.length; j++ ) {
                stm.setEntry(i, j, stm.getEntry(i, j) * scale[j] / scale[i]);
            }
        }

        // Return the error state transition matrix
        return stm;

    }

    /** Get the normalized measurement matrix H.
     * H contains the partial derivatives of the measurement with respect to the state.
     * H is an nxm matrix where n is the size of the measurement vector and m the size of the state vector.
     * @return the normalized measurement matrix H
     */
    private RealMatrix getMeasurementMatrix() {

        // Observed measurement characteristics
        final SpacecraftState        evaluationState     = predictedMeasurement.getStates()[0];
        final ObservedMeasurement<?> observedMeasurement = predictedMeasurement.getObservedMeasurement();
        final double[] sigma  = predictedMeasurement.getObservedMeasurement().getTheoreticalStandardDeviation();

        // Initialize measurement matrix H: nxm
        // n: Number of measurements in current measurement
        // m: State vector size
        final RealMatrix measurementMatrix = MatrixUtils.
                createRealMatrix(observedMeasurement.getDimension(),
                                 correctedEstimate.getState().getDimension());

        // Predicted orbit
        final Orbit predictedOrbit = evaluationState.getOrbit();

        // Measurement matrix's columns related to orbital and propagation parameters
        // ----------------------------------------------------------

        // Partial derivatives of the current Cartesian coordinates with respect to current orbital state
        final int nbOrb  = getNumberSelectedOrbitalDrivers();
        final int nbProp = getNumberSelectedPropagationDrivers();
        final double[][] aCY = new double[nbOrb][nbOrb];
        predictedOrbit.getJacobianWrtParameters(builder.getPositionAngleType(), aCY);
        final RealMatrix dCdY = new Array2DRowRealMatrix(aCY, false);

        // Jacobian of the measurement with respect to current Cartesian coordinates
        final RealMatrix dMdC = new Array2DRowRealMatrix(predictedMeasurement.getStateDerivatives(0), false);

        // Jacobian of the measurement with respect to current orbital state
        RealMatrix dMdY = dMdC.multiply(dCdY);

        // Compute factor dShortPeriod_dMeanState = I+B1 | B4
        final RealMatrix IpB1B4 = MatrixUtils.createRealMatrix(nbOrb, nbOrb + nbProp);

        // B1
        final RealMatrix B1 = harvester.getB1();

        // I + B1
        final RealMatrix I = MatrixUtils.createRealIdentityMatrix(nbOrb);
        final RealMatrix IpB1 = I.add(B1);
        IpB1B4.setSubMatrix(IpB1.getData(), 0, 0);

        // If there are not propagation parameters, B4 is null
        if (psiS != null) {
            final RealMatrix B4 = harvester.getB4();
            IpB1B4.setSubMatrix(B4.getData(), 0, nbOrb);
        }

        // Ref [1], Eq. 3.10
        dMdY = dMdY.multiply(IpB1B4);

        for (int i = 0; i < dMdY.getRowDimension(); i++) {
            for (int j = 0; j < nbOrb; j++) {
                final double driverScale = builder.getOrbitalParametersDrivers().getDrivers().get(j).getScale();
                measurementMatrix.setEntry(i, j, dMdY.getEntry(i, j) / sigma[i] * driverScale);
            }

            int col = 0;
            for (int j = 0; j < nbProp; j++) {
                final double driverScale = estimatedPropagationParameters.getDrivers().get(j).getScale();
                for (Span<Double> span = estimatedPropagationParameters.getDrivers().get(j).getValueSpanMap().getFirstSpan();
                                  span != null; span = span.next()) {

                    measurementMatrix.setEntry(i, col + nbOrb,
                                               dMdY.getEntry(i, col + nbOrb) / sigma[i] * driverScale);
                    col++;
                }
            }
        }

        // Normalized measurement matrix's columns related to measurement parameters
        // --------------------------------------------------------------

        // Jacobian of the measurement with respect to measurement parameters
        // Gather the measurement parameters linked to current measurement
        for (final ParameterDriver driver : observedMeasurement.getParametersDrivers()) {
            if (driver.isSelected()) {
                for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
                    // Derivatives of current measurement w/r to selected measurement parameter
                    final double[] aMPm = predictedMeasurement.getParameterDerivatives(driver, span.getStart());

                    // Check that the measurement parameter is managed by the filter
                    if (measurementParameterColumns.get(span.getData()) != null) {
                        // Column of the driver in the measurement matrix
                        final int driverColumn = measurementParameterColumns.get(span.getData());

                        // Fill the corresponding indexes of the measurement matrix
                        for (int i = 0; i < aMPm.length; ++i) {
                            measurementMatrix.setEntry(i, driverColumn, aMPm[i] / sigma[i] * driver.getScale());
                        }
                    }
                }
            }
        }

        return measurementMatrix;
    }

    /** Predict the filter correction for the new observation.
     * @param stm normalized state transition matrix
     * @return the predicted filter correction for the new observation
     */
    private RealVector predictFilterCorrection(final RealMatrix stm) {
        // Ref [1], Eq. 3.5a and 3.5b
        return stm.operate(correctedFilterCorrection);
    }

    /** Compute the predicted osculating elements.
     * @param filterCorrection kalman filter correction
     * @return the predicted osculating element
     */
    private double[] computeOsculatingElements(final RealVector filterCorrection) {

        // Number of estimated orbital parameters
        final int nbOrb = getNumberSelectedOrbitalDrivers();

        // B1
        final RealMatrix B1 = harvester.getB1();

        // Short periodic terms
        final double[] shortPeriodTerms = dsstPropagator.getShortPeriodTermsValue(nominalMeanSpacecraftState);

        // Physical filter correction
        final RealVector physicalFilterCorrection = MatrixUtils.createRealVector(nbOrb);
        for (int index = 0; index < nbOrb; index++) {
            physicalFilterCorrection.addToEntry(index, filterCorrection.getEntry(index) * scale[index]);
        }

        // B1 * physicalCorrection
        final RealVector B1Correction = B1.operate(physicalFilterCorrection);

        // Nominal mean elements
        final double[] nominalMeanElements = new double[nbOrb];
        OrbitType.EQUINOCTIAL.mapOrbitToArray(nominalMeanSpacecraftState.getOrbit(), builder.getPositionAngleType(), nominalMeanElements, null);

        // Ref [1] Eq. 3.6
        final double[] osculatingElements = new double[nbOrb];
        for (int i = 0; i < nbOrb; i++) {
            osculatingElements[i] = nominalMeanElements[i] +
                                    physicalFilterCorrection.getEntry(i) +
                                    shortPeriodTerms[i] +
                                    B1Correction.getEntry(i);
        }

        // Return
        return osculatingElements;

    }

    /** Analytical computation of derivatives.
     * This method allow to compute analytical derivatives.
     * @param state mean state used to calculate short period perturbations
     */
    private void analyticalDerivativeComputations(final SpacecraftState state) {
        harvester.setReferenceState(state);
    }

    /** Get the number of estimated orbital parameters.
     * @return the number of estimated orbital parameters
     */
    private int getNumberSelectedOrbitalDrivers() {
        return estimatedOrbitalParameters.getNbParams();
    }

    /** Get the number of estimated propagation parameters.
     * @return the number of estimated propagation parameters
     */
    private int getNumberSelectedPropagationDrivers() {
        return estimatedPropagationParameters.getNbParams();
    }

    /** Get the number of estimated orbital parameters values (some parameter
     * driver may have several values to estimate for different time range
     * {@link ParameterDriver}.
     * @return the number of estimated values for , orbital parameters
     */
    private int getNumberSelectedOrbitalDriversValuesToEstimate() {
        int nbOrbitalValuesToEstimate = 0;
        for (final ParameterDriver driver : estimatedOrbitalParameters.getDrivers()) {
            nbOrbitalValuesToEstimate += driver.getNbOfValues();
        }
        return nbOrbitalValuesToEstimate;
    }

    /** Get the number of estimated propagation parameters values (some parameter
     * driver may have several values to estimate for different time range
     * {@link ParameterDriver}.
     * @return the number of estimated values for propagation parameters
     */
    private int getNumberSelectedPropagationDriversValuesToEstimate() {
        int nbPropagationValuesToEstimate = 0;
        for (final ParameterDriver driver : estimatedPropagationParameters.getDrivers()) {
            nbPropagationValuesToEstimate += driver.getNbOfValues();
        }
        return nbPropagationValuesToEstimate;
    }

    /** Get the number of estimated measurement parameters values (some parameter
     * driver may have several values to estimate for different time range
     * {@link ParameterDriver}.
     * @return the number of estimated values for measurement parameters
     */
    private int getNumberSelectedMeasurementDriversValuesToEstimate() {
        int nbMeasurementValuesToEstimate = 0;
        for (final ParameterDriver driver : estimatedMeasurementsParameters.getDrivers()) {
            nbMeasurementValuesToEstimate += driver.getNbOfValues();
        }
        return nbMeasurementValuesToEstimate;
    }

    /** Update the estimated parameters after the correction phase of the filter.
     * The min/max allowed values are handled by the parameter themselves.
     */
    private void updateParameters() {
        final RealVector correctedState = correctedEstimate.getState();
        int i = 0;
        // Orbital parameters
        for (final DelegatingDriver driver : getEstimatedOrbitalParameters().getDrivers()) {
            // let the parameter handle min/max clipping
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                driver.setNormalizedValue(driver.getNormalizedValue(span.getStart()) + correctedState.getEntry(i++), span.getStart());
            }
        }

        // Propagation parameters
        for (final DelegatingDriver driver : getEstimatedPropagationParameters().getDrivers()) {
            // let the parameter handle min/max clipping
            // If the parameter driver contains only 1 value to estimate over the all time range
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                driver.setNormalizedValue(driver.getNormalizedValue(span.getStart()) + correctedState.getEntry(i++), span.getStart());
            }
        }

        // Measurements parameters
        for (final DelegatingDriver driver : getEstimatedMeasurementsParameters().getDrivers()) {
            // let the parameter handle min/max clipping
            for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
                driver.setNormalizedValue(driver.getNormalizedValue(span.getStart()) + correctedState.getEntry(i++), span.getStart());
            }
        }
    }

}