Estimate Measurement Parameters in the Unscented Kalman Model

Hello everyone!

Recently, I’ve been implementing the Unscented Kalman Filter (UKF) for attitude estimations using parameter drivers (quaternions) obtained from magnitude measurements. While working with the latest version of Orekit 12.0.1 and utilizing the UKF, I’ve encountered an issue where the model doesn’t allow estimating measurements without first estimating orbital parameters.

In the UnscentedKalmanModel.java class, the covarianceIndirection is created through a new int[covarianceMatrixProviders.size()][columns] as shown in the image. The problem arises because I’ve modified the code to skip the estimation of both orbital and propagation parameters, focusing solely on magnitude estimation, specifically the 4 quaternions. This causes the value of columns to be 4. The fact that columns is set to 4 makes covarianceIndirection[k][i++] in the first for (final ParameterDriver driver : orbitDrivers.getDrivers()) result in an error when i=4:

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 4

Given the current code structure, I’m unsure if I can perform measurements estimation without estimating orbital parameters. In the case of propagation parameters, the code detects that c = null, so it doesn’t add values to covarianceIndirection.

Is there a way to resolve this issue?

Thank you in advance, everyone!

Hi @diegoth99

Welcome to the Orekit forum! :slight_smile:

Does it work with the extended Kalman filter?

If yes, we shall think about doing the same work in the unscented filter.

Regards,
Bryan

Before using the extended filter could you try using builder.deselectDynamicParameters() before Initializing your filter.
This method is used to disable the estimation of the orbital and propagation parameter from the propagator point of view.

Bryan

Hello! Thank you very much for your prompt response @bcazabonne! :slightly_smiling_face:

I’ve tried using the Extended Kalman Filter; however, a similar issue arises since there are no orbital parameters to estimate in the SemiAnalyticalKalmanModel.java. You can see that an error (matrix must have at least one row) will occur as there are no orbital parameters to estimate.

// Initialize inverse of the orbital part of the state transition matrix
this.phiS = MatrixUtils.createRealIdentityMatrix(getNumberSelectedOrbitalDriversValuesToEstimate());

The only estimator that seems to work correctly with magnitude measurements is the KalmanEstimator. As soon as I use the UnscentedKalmanEstimator, the problem described in the previous post occurs. Below, I show in the following image one of the key differences between the Unscented Kalman Model and the Kalman Model in this process.

On the left is the Unscented Kalman Model, while on the right is the Kalman Model. There is a clear difference between lines 227-230 of the UKM and lines 221-226 of the KM. It can be observed that in the case of the KM, it works because it first detects that c != null before adding the value to covarianceIndirection. In the case of the UKM, the value of i increases even if c == null, causing the model to stop working when the number of the four quaternions I want to measure is exceeded (which defines the size of columns = 4). This leads to the problem mentioned in the previous post.

On the other hand, it’s worth mentioning that I’ve tried using builder.deselectDynamicParameters(), but I’m using an EphemerisPropagatorBuilder instead of a NumericalPropagatorBuilder as in previous versions. However, I was already performing this treatment previously before the builder:

    for (final ParameterDriver parameterDriver : propagatorBuilder.getOrbitalParametersDrivers().getDrivers())
        {parameterDriver.setSelected(false);}
    for (final ParameterDriver parameterDriver : propagatorBuilder.getPropagationParametersDrivers().getDrivers())
        {parameterDriver.setSelected(false);}

I appreciate your time in advance for the offered assistance. Best regards, and I look forward to your response!

SemiAnalyticalKalmanModel is not the classical Extended Kalman Filter (EKF). It is an EKF adapted to semi analytical propagators.
It is the dark side of the black magic of the Kalman filters. :slight_smile:

Great! This class is the classical EKF.
So, if the EKF is able to perform the estimation without orbital and propagation parameters, the UKF shall do it too.
Could you open an issue in our Gitlab repository?

Just note an important information. Even if the UKF code is adapted, I don’t think it will work with the current version of Orekit because of the current issue: Improve estimation of propagation and measurement parameters with Unscented Kalman Filter (#1036) · Issues · Orekit / Orekit · GitLab

These are two different issues that shall be fixed separately.

Thank you,
Bryan

Hi @bcazabonne, I am running into this same error when building an EKF using a Numerical Propagator.

My EKF class looks like this:

class ExtendedKalmanFilter(
    private val propagatorBuilder: PropagatorBuilder,
    private val station: GroundStation,
    private val frame: Frame,
    private val noiseModel: NoiseModel
) {
    private lateinit var kalmanEstimator: KalmanEstimator

fun initializeKalmanEstimator() {
        val choleskyDecomposer = CholeskyDecomposer(1E-12, 1E-12)
        val initialCovariance = DiagonalMatrix(doubleArrayOf(10000.0, 10000.0, 10000.0, 1.0, 1.0, 1.0))
        val initialProcessNoise = DiagonalMatrix(doubleArrayOf(1e-10, 1e-10, 1e-10, 1e-10, 1e-10, 1e-10))

        val builder = KalmanEstimatorBuilder()
        builder.addPropagationConfiguration(propagatorBuilder(), ConstantProcessNoise(initialCovariance, initialProcessNoise))
        builder.decomposer(choleskyDecomposer)

        kalmanEstimator = builder.build()
    }

    private fun propagatorBuilder() = propagatorBuilder

    fun estimate(measurement: AngularRaDec) {
        val estimatedPropagators = kalmanEstimator.estimationStep(measurement)
    }

    fun getEstimatedState(): RealVector {
        return kalmanEstimator.physicalEstimatedState
    }

    fun getEstimatedCovariance(): RealMatrix {
        return kalmanEstimator.physicalEstimatedCovarianceMatrix
    }
}

This error occurs when I call kalmanEstimator = builder.build() in my initialization. How do I resolve this issue?

I have created the following tests and when I run testKalmanInitialization, I run into the error. I am unsure what other parameters need to be defined. I thought it may have something to do with the “drivers” or the normalized parameters but my understanding of both of these is lacking for sure.

class ExtendedKalmanFilterTest {
    private lateinit var ekf: ExtendedKalmanFilter
    private lateinit var propagatorBuilder: PropagatorBuilder
    private lateinit var groundStation: GroundStation
    private lateinit var frame: Frame
    private lateinit var noiseModel: NoiseModel

    companion object {
        val initialOrbit: Orbit = CartesianOrbit(
            PVCoordinates(
                Vector3D(6871.0e3, 0.0, 0.0),
                Vector3D(0.0, 7.8e3, 0.0)
            ),
            FramesFactory.getEME2000(),
            AbsoluteDate.J2000_EPOCH,
            Constants.EGM96_EARTH_MU
        )
    }

    @BeforeEach
    fun setUp() {
        val dataDirectory = File("src/main/resources/orekit-data-master.zip")
        System.setProperty(DataProvidersManager.OREKIT_DATA_PATH, dataDirectory.absolutePath)

        propagatorBuilder = NumericalPropagatorBuilder(initialOrbit)
        frame = FramesFactory.getEME2000()
        noiseModel = NoiseModel()

        val node = Node(
            id = UUID.fromString("07dab967-fefb-4671-a229-3ce22f277850"),
            name = "OurSky Node0_6b",
            location = Point(
                -77.919167,
                39.095472,
                224.9
            ),
            cameraId = UUID.fromString("dff9fc6e-97e2-4fdc-986b-ed719511a5f2"),
            createdBy = UUID.fromString("0fab28b9-fa97-4836-b0d8-f205d364bb96"),
            diagnosticsEnabled = true,
            elevationMask = null,
            minAltitude = 20,
            mountId = UUID.fromString("2306d93b-1c35-4c74-a1ca-cb140fc0b264"),
            observatoryId = null,
            opticalTubeId = UUID.fromString("3b1243a8-3272-49df-996a-c276f74cf732"),
            organizationId = UUID.fromString("140fc9e4-ea1d-4452-941f-8bda318aec0c"),
            slewTiming = null,
            state = NodeState.READY,
            createdAt = OffsetDateTime.now()
        )

        val topoFrame = topoFrameForPoint(node.location, node.name)
        groundStation = GroundStation(topoFrame)

        ekf = ExtendedKalmanFilter(propagatorBuilder, groundStation, frame, noiseModel)
        ekf.initializeKalmanEstimator()
    }

    @Test
    fun testKalmanInitialization() {
        assertDoesNotThrow { ekf.initializeKalmanEstimator() }
    }

    @Test
    fun testEstimation() {
        val initialPosition = initialOrbit.pvCoordinates.position

        val ra = FastMath.atan2(initialPosition.y, initialPosition.x)
        val dec = FastMath.atan2(initialPosition.z, FastMath.sqrt(initialPosition.x * initialPosition.x + initialPosition.y * initialPosition.y))

        val error = 5.0 * FastMath.PI / (180.0 * 3600.0) // 5 arcseconds in radians
        val raWithError = ra + error
        val decWithError = dec + error

        val date = AbsoluteDate.J2000_EPOCH.shiftedBy(1000.0)
        val sensorData = SensorData("node-1", listOf(0.1, 0.2), listOf(0.05, 0.06), hasGPSTiming = true)

        val measurementWithError = ekf.createMeasurement(date, raWithError, decWithError, sensorData, 5.0)

        assertDoesNotThrow { ekf.estimate(measurementWithError) }
    }

    @Test
    fun testGetEstimatedState() {
        assertDoesNotThrow {
            val estimatedState = ekf.getEstimatedState()
            assertNotNull(estimatedState)
        }
    }

    @Test
    fun testGetEstimatedCovariance() {
        assertDoesNotThrow {
            val covarianceMatrix = ekf.getEstimatedCovariance()
            assertNotNull(covarianceMatrix)
        }
    }
}

Another thought was that I may be building my propagator incorrectly?

class NumericalPropagatorBuilder(var initialOrbit: Orbit) : PropagatorBuilder {

    private val frame: Frame = FramesFactory.getEME2000()
    private val mu: Double = Constants.EGM96_EARTH_MU

    private val integrator: DormandPrince853Integrator
    private val numericalPropagator: NumericalPropagator

    init {
        val minStep = 0.001
        val maxStep = 500.0
        val positionError = 0.001
        val initialOrbit = initialOrbit
        val orbitType = OrbitType.CARTESIAN
        val tolerance = NumericalPropagator.tolerances(positionError, initialOrbit, orbitType)
        integrator = DormandPrince853Integrator(minStep, maxStep, tolerance[0], tolerance[1])

        numericalPropagator = NumericalPropagator(integrator)
        numericalPropagator.setMu(Constants.EGM96_EARTH_MU)
        numericalPropagator.initialState = SpacecraftState(initialOrbit)

        val gravityField = GravityFieldFactory.getNormalizedProvider(12, 12)
        val gravityForceModel = HolmesFeatherstoneAttractionModel(
            FramesFactory.getEME2000(),
            gravityField
        )
        numericalPropagator.addForceModel(gravityForceModel)
    }

    override fun buildPropagator(normalizedParameters: DoubleArray): Propagator {
        return numericalPropagator
    }

    override fun copy(): PropagatorBuilder {
        return NumericalPropagatorBuilder(initialOrbit)
    }

    override fun getFrame(): Frame {
        return frame
    }

    override fun getInitialOrbitDate(): AbsoluteDate {
        return initialOrbit.date
    }

    override fun getMu(): Double {
        return mu
    }

    override fun getOrbitalParametersDrivers(): ParameterDriversList {
        return ParameterDriversList()
    }

    override fun getOrbitType(): OrbitType {
        return OrbitType.CARTESIAN
    }

    override fun getPositionAngleType(): PositionAngleType {
        return PositionAngleType.MEAN
    }

    override fun getPropagationParametersDrivers(): ParameterDriversList {
        return ParameterDriversList()
    }

    override fun getSelectedNormalizedParameters(): DoubleArray {
        return doubleArrayOf()
    }

    override fun resetOrbit(newOrbit: Orbit) {
        this.initialOrbit = newOrbit
    }

    // dummy BLS
    override fun buildLeastSquaresModel(
        builders: Array<PropagatorBuilder>,
        measurements: MutableList<ObservedMeasurement<*>>,
        estimatedMeasurementsParameters: ParameterDriversList,
        observer: ModelObserver
    ): AbstractBatchLSModel? {
        return null
    }
}

Apologies for the very long response 8 months after the fact. Thank you in advance! Also, I am using Kotlin so that should explain the funky syntax!

Hi there,

I think what you’re missing is the call to setSelected(true) on your orbital drivers. Otherwise here you’re estimating nothing.
Note that you’ll need to do the same for any other parameter you want to include.

Cheers,
Romain.

Hi Romain,

Thank you for your input! You were right that the issue was my drivers.

I ended up adding this get function to my NumericalPropagatorBuilder class:

    override fun getOrbitalParametersDrivers(): ParameterDriversList {
        val drivers = ParameterDriversList()

        val xDriver = ParameterDriver("x", initialOrbit.pvCoordinates.position.x, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)
        val yDriver = ParameterDriver("y", initialOrbit.pvCoordinates.position.y, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)
        val zDriver = ParameterDriver("z", initialOrbit.pvCoordinates.position.z, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)

        val vxDriver = ParameterDriver("vx", initialOrbit.pvCoordinates.velocity.x, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)
        val vyDriver = ParameterDriver("vy", initialOrbit.pvCoordinates.velocity.y, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)
        val vzDriver = ParameterDriver("vz", initialOrbit.pvCoordinates.velocity.z, 1.0, Double.MIN_VALUE, Double.MAX_VALUE)

        xDriver.isSelected = true
        yDriver.isSelected = true
        zDriver.isSelected = true
        vxDriver.isSelected = true
        vyDriver.isSelected = true
        vzDriver.isSelected = true

        drivers.add(xDriver)
        drivers.add(yDriver)
        drivers.add(zDriver)
        drivers.add(vxDriver)
        drivers.add(vyDriver)
        drivers.add(vzDriver)

        return drivers
    }

and this driver initialization to my EKF initialization:

val orbitalParameterDrivers = numericalPropagatorBuilder.getOrbitalParametersDrivers()
        for (driver in orbitalParameterDrivers.drivers) {
            driver.isSelected = true
        }

Perhaps overly complicated but it appears to have worked!

Cheers,
CJM