Commit 8e6c4163 authored by Bryan Cazabonne's avatar Bryan Cazabonne
Browse files

Merge branch 'issue-726' into 'develop'

Added ephemeris based estimation.

Closes #726

See merge request !300
parents ca0ae2ea 639bc990
Pipeline #2485 passed with stages
in 14 minutes and 50 seconds
......@@ -21,6 +21,9 @@
</properties>
<body>
<release version="11.3" date="TBD" description="TBD">
<action dev="bryan" type="update" issue="726">
Added ephemeris based estimation.
</action>
<action dev="maxime" type="update" issue="955">
Added method to get measurement types.
</action>
......
......@@ -256,7 +256,7 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
// Store providers for process noise matrices
this.covarianceMatricesProviders = covarianceMatricesProviders;
this.measurementProcessNoiseMatrix = measurementProcessNoiseMatrix;
this.covarianceIndirection = new int[covarianceMatricesProviders.size()][columns];
this.covarianceIndirection = new int[builders.size()][columns];
for (int k = 0; k < covarianceIndirection.length; ++k) {
final ParameterDriversList orbitDrivers = builders.get(k).getOrbitalParametersDrivers();
final ParameterDriversList parametersDrivers = builders.get(k).getPropagationParametersDrivers();
......@@ -264,7 +264,9 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
int i = 0;
for (final ParameterDriver driver : orbitDrivers.getDrivers()) {
final Integer c = orbitalParameterColumns.get(driver.getName());
covarianceIndirection[k][i++] = (c == null) ? -1 : c.intValue();
if (c != null) {
covarianceIndirection[k][i++] = c.intValue();
}
}
for (final ParameterDriver driver : parametersDrivers.getDrivers()) {
final Integer c = propagationParameterColumns.get(driver.getName());
......@@ -329,9 +331,11 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
// Covariance matrix
final RealMatrix noiseK = MatrixUtils.createRealMatrix(nbDyn + nbMeas, nbDyn + nbMeas);
final RealMatrix noiseP = covarianceMatricesProviders.get(k).
getInitialCovarianceMatrix(correctedSpacecraftStates[k]);
noiseK.setSubMatrix(noiseP.getData(), 0, 0);
if (nbDyn > 0) {
final RealMatrix noiseP = covarianceMatricesProviders.get(k).
getInitialCovarianceMatrix(correctedSpacecraftStates[k]);
noiseK.setSubMatrix(noiseP.getData(), 0, 0);
}
if (measurementProcessNoiseMatrix != null) {
final RealMatrix noiseM = measurementProcessNoiseMatrix.
getInitialCovarianceMatrix(correctedSpacecraftStates[k]);
......@@ -392,7 +396,12 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
// count parameters, taking care of counting all orbital parameters
// regardless of them being estimated or not
int requiredDimension = orbitalParameters.getNbParams();
int requiredDimension = 0;
for (final ParameterDriver driver : orbitalParameters.getDrivers()) {
if (driver.isSelected()) {
++requiredDimension;
}
}
for (final ParameterDriver driver : propagationParameters.getDrivers()) {
if (driver.isSelected()) {
++requiredDimension;
......@@ -702,12 +711,13 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
// Indexes
final int[] indK = covarianceIndirection[k];
// Reset reference (for example compute short periodic terms in DSST)
harvesters[k].setReferenceState(predictedSpacecraftStates[k]);
// Derivatives of the state vector with respect to initial state vector
final int nbOrbParams = estimatedOrbitalParameters[k].getNbParams();
if (nbOrbParams > 0) {
// Reset reference (for example compute short periodic terms in DSST)
harvesters[k].setReferenceState(predictedSpacecraftStates[k]);
final RealMatrix dYdY0 = harvesters[k].getStateTransitionMatrix(predictedSpacecraftStates[k]);
// Fill upper left corner (dY/dY0)
......@@ -985,10 +995,12 @@ public abstract class AbstractKalmanModel implements KalmanEstimation, NonLinear
// Covariance matrix
final RealMatrix noiseK = MatrixUtils.createRealMatrix(nbDyn + nbMeas, nbDyn + nbMeas);
final RealMatrix noiseP = covarianceMatricesProviders.get(k).
getProcessNoiseMatrix(correctedSpacecraftStates[k],
predictedSpacecraftStates[k]);
noiseK.setSubMatrix(noiseP.getData(), 0, 0);
if (nbDyn > 0) {
final RealMatrix noiseP = covarianceMatricesProviders.get(k).
getProcessNoiseMatrix(correctedSpacecraftStates[k],
predictedSpacecraftStates[k]);
noiseK.setSubMatrix(noiseP.getData(), 0, 0);
}
if (measurementProcessNoiseMatrix != null) {
final RealMatrix noiseM = measurementProcessNoiseMatrix.
getProcessNoiseMatrix(correctedSpacecraftStates[k],
......
......@@ -23,6 +23,7 @@ import org.hipparchus.linear.MatrixDecomposer;
import org.hipparchus.linear.QRDecomposer;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.propagation.conversion.EphemerisPropagatorBuilder;
import org.orekit.propagation.conversion.OrbitDeterminationPropagatorBuilder;
import org.orekit.utils.ParameterDriversList;
......@@ -94,16 +95,19 @@ public class KalmanEstimatorBuilder {
* <p>
* The {@code provider} should return a matrix with dimensions and ordering
* consistent with the {@code builder} configuration. The first 6 rows/columns
* correspond to the 6 orbital parameters which must all be present, regardless
* of the fact they are estimated or not. The remaining elements correspond
* correspond to the 6 orbital parameters. The remaining elements correspond
* to the subset of propagation parameters that are estimated, in the
* same order as propagatorBuilder.{@link
* org.orekit.propagation.conversion.PropagatorBuilder#getPropagationParametersDrivers()
* getPropagationParametersDrivers()}.{@link org.orekit.utils.ParameterDriversList#getDrivers()
* getDrivers()} (but filtering out the non selected drivers).
* </p>
* @param builder The propagator builder to use in the Kalman filter.
* @param builder The propagator builder to use in the Kalman filter.
* @param provider The process noise matrices provider to use, consistent with the builder.
* This parameter can be equal to {@code null} if the input builder is
* an {@link EphemerisPropagatorBuilder}. Indeed, for ephemeris based estimation
* only measurement parameters are estimated. Therefore, the covariance related
* to dynamical parameters can be null.
* @return this object.
* @see CovarianceMatrixProvider#getProcessNoiseMatrix(org.orekit.propagation.SpacecraftState,
* org.orekit.propagation.SpacecraftState) getProcessNoiseMatrix(previous, current)
......
......@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.exception.MathIllegalArgumentException;
import org.hipparchus.linear.RealMatrix;
import org.hipparchus.util.FastMath;
import org.orekit.attitudes.Attitude;
import org.orekit.attitudes.AttitudeProvider;
......@@ -30,6 +31,7 @@ import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.orbits.Orbit;
import org.orekit.propagation.AbstractMatricesHarvester;
import org.orekit.propagation.BoundedPropagator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.time.AbsoluteDate;
......@@ -293,6 +295,15 @@ public class Ephemeris extends AbstractAnalyticalPropagator implements BoundedPr
return managed;
}
/** {@inheritDoc} */
@Override
protected AbstractMatricesHarvester createHarvester(final String stmName, final RealMatrix initialStm,
final DoubleArrayDictionary initialJacobianColumns) {
// In order to not throw an Orekit exception during ephemeris based orbit determination
// The default behavior of the method is overrided to return a null parameter
return null;
}
/** Internal PVCoordinatesProvider for attitude computation. */
private static class LocalPVProvider implements PVCoordinatesProvider, Serializable {
......
/* Copyright 2022 Bryan Cazabonne
* 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.
* Bryan Cazabonne 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.propagation.conversion;
import java.util.List;
import org.orekit.attitudes.AttitudeProvider;
import org.orekit.estimation.leastsquares.AbstractBatchLSModel;
import org.orekit.estimation.leastsquares.BatchLSModel;
import org.orekit.estimation.leastsquares.ModelObserver;
import org.orekit.estimation.measurements.ObservedMeasurement;
import org.orekit.estimation.sequential.AbstractKalmanModel;
import org.orekit.estimation.sequential.CovarianceMatrixProvider;
import org.orekit.estimation.sequential.KalmanModel;
import org.orekit.orbits.PositionAngle;
import org.orekit.propagation.Propagator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.analytical.Ephemeris;
import org.orekit.utils.ParameterDriversList;
/** Builder for Ephemeris propagator.
* @author Bryan Cazabonne
* @since 11.3
*/
public class EphemerisPropagatorBuilder extends AbstractPropagatorBuilder implements OrbitDeterminationPropagatorBuilder {
/** Default position scale (not used for ephemeris based estimation). */
private static final double DEFAULT_SCALE = 10.0;
/** List of spacecraft states. */
private final List<SpacecraftState> states;
/** The extrapolation threshold beyond which the propagation will fail. **/
private final double extrapolationThreshold;
/** Number of points to use in interpolation. */
private final int interpolationPoints;
/** Attitude provider. */
private final AttitudeProvider provider;
/** Constructor.
* @param states list of spacecraft states
* @param interpolationPoints number of points to use in interpolation
* @param extrapolationThreshold the extrapolation threshold beyond which the propagation will fail
* @param attitudeProvider attitude provider
*/
public EphemerisPropagatorBuilder(final List<SpacecraftState> states,
final int interpolationPoints,
final double extrapolationThreshold,
final AttitudeProvider attitudeProvider) {
super(states.get(0).getOrbit(), PositionAngle.TRUE, DEFAULT_SCALE, false, attitudeProvider);
deselectDynamicParameters();
this.states = states;
this.interpolationPoints = interpolationPoints;
this.extrapolationThreshold = extrapolationThreshold;
this.provider = attitudeProvider;
}
/** {@inheritDoc}. */
@Override
public Propagator buildPropagator(final double[] normalizedParameters) {
return new Ephemeris(states, interpolationPoints, extrapolationThreshold, provider);
}
/** {@inheritDoc} */
@Override
public AbstractBatchLSModel buildLSModel(final OrbitDeterminationPropagatorBuilder[] builders,
final List<ObservedMeasurement<?>> measurements,
final ParameterDriversList estimatedMeasurementsParameters,
final ModelObserver observer) {
return new BatchLSModel(builders, measurements, estimatedMeasurementsParameters, observer);
}
/** {@inheritDoc} */
@Override
public AbstractKalmanModel buildKalmanModel(final List<OrbitDeterminationPropagatorBuilder> propagatorBuilders,
final List<CovarianceMatrixProvider> covarianceMatricesProviders,
final ParameterDriversList estimatedMeasurementsParameters,
final CovarianceMatrixProvider measurementProcessNoiseMatrix) {
return new KalmanModel(propagatorBuilders, covarianceMatricesProviders, estimatedMeasurementsParameters, measurementProcessNoiseMatrix);
}
}
......@@ -208,6 +208,7 @@
station position, pole motion and rate, prime meridian correction and rate, total zenith
delay in tropospheric correction)
* orbit determination can be performed with numerical, DSST, SDP4/SGP4, Eckstein-Hechler, Brouwer-Lyddane, or Keplerian propagators
* ephemeris-based orbit determination to estimate measurement parameters like station biases or clock offsets
* multi-satellites orbit determination
* initial orbit determination methods (Gibbs, Gooding, Lambert and Laplace)
* ground stations displacements due to solid tides
......
/* Copyright 2002-2022 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;
import java.util.Arrays;
import java.util.List;
import org.hipparchus.util.FastMath;
import org.orekit.bodies.CelestialBody;
import org.orekit.bodies.CelestialBodyFactory;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.estimation.measurements.GroundStation;
import org.orekit.forces.gravity.potential.UnnormalizedSphericalHarmonicsProvider;
import org.orekit.frames.EOPHistory;
import org.orekit.frames.FramesFactory;
import org.orekit.frames.TopocentricFrame;
import org.orekit.models.earth.displacement.StationDisplacement;
import org.orekit.models.earth.displacement.TidalDisplacement;
import org.orekit.orbits.Orbit;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScalesFactory;
import org.orekit.time.UT1Scale;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;
public class EphemerisContext implements StationDataProvider {
public IERSConventions conventions;
public OneAxisEllipsoid earth;
public CelestialBody sun;
public CelestialBody moon;
public UnnormalizedSphericalHarmonicsProvider gravity;
public TimeScale utc;
public UT1Scale ut1;
public Orbit initialOrbit;
public StationDisplacement[] displacements;
public List<GroundStation> stations;
public EphemerisContext() {
this.conventions = IERSConventions.IERS_2010;
this.earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
Constants.WGS84_EARTH_FLATTENING,
FramesFactory.getITRF(conventions, true));
final EOPHistory eopHistory = FramesFactory.getEOPHistory(conventions, true);
this.ut1 = TimeScalesFactory.getUT1(eopHistory);
this.displacements = new StationDisplacement[] {
new TidalDisplacement(Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
Constants.JPL_SSD_SUN_EARTH_PLUS_MOON_MASS_RATIO,
Constants.JPL_SSD_EARTH_MOON_MASS_RATIO,
CelestialBodyFactory.getSun(), CelestialBodyFactory.getMoon(),
conventions, false)
};
this.stations = Arrays.asList(createStation(-53.05388, -75.01551, 1750.0, "Isla Desolación"),
createStation( 62.29639, -7.01250, 880.0, "Slættaratindur"));
}
GroundStation createStation(double latitudeInDegrees, double longitudeInDegrees,
double altitude, String name) {
final GeodeticPoint gp = new GeodeticPoint(FastMath.toRadians(latitudeInDegrees),
FastMath.toRadians(longitudeInDegrees),
altitude);
return new GroundStation(new TopocentricFrame(earth, gp, name),
ut1.getEOPHistory(), displacements);
}
@Override
public List<GroundStation> getStations() {
return stations;
}
}
/* Copyright 2022 Bryan Cazabonne
* 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.
* Bryan Cazabonne 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.leastsquares;
import java.util.ArrayList;
import java.util.List;
import org.hipparchus.optim.nonlinear.vector.leastsquares.LevenbergMarquardtOptimizer;
import org.hipparchus.util.FastMath;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.orekit.Utils;
import org.orekit.errors.OrekitException;
import org.orekit.estimation.EphemerisContext;
import org.orekit.estimation.KeplerianEstimationTestUtils;
import org.orekit.estimation.measurements.AngularAzEl;
import org.orekit.estimation.measurements.AngularAzElMeasurementCreator;
import org.orekit.estimation.measurements.ObservedMeasurement;
import org.orekit.estimation.measurements.Range;
import org.orekit.estimation.measurements.RangeMeasurementCreator;
import org.orekit.estimation.measurements.RangeRateMeasurementCreator;
import org.orekit.estimation.measurements.modifiers.Bias;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.orbits.KeplerianOrbit;
import org.orekit.orbits.Orbit;
import org.orekit.orbits.PositionAngle;
import org.orekit.propagation.Propagator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.analytical.Ephemeris;
import org.orekit.propagation.analytical.KeplerianPropagator;
import org.orekit.propagation.conversion.EphemerisPropagatorBuilder;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScalesFactory;
public class EphemerisBatchLSEstimatorTest {
private AbsoluteDate initDate;
private AbsoluteDate finalDate;
private Frame inertialFrame;
private Propagator propagator;
private EphemerisContext context;
@BeforeEach
public void setUp() throws IllegalArgumentException, OrekitException {
Utils.setDataRoot("regular-data");
initDate = new AbsoluteDate(new DateComponents(2004, 01, 01),
TimeComponents.H00,
TimeScalesFactory.getUTC());
finalDate = new AbsoluteDate(new DateComponents(2004, 01, 02),
TimeComponents.H00,
TimeScalesFactory.getUTC());
double a = 7187990.1979844316;
double e = 0.5e-4;
double i = 1.7105407051081795;
double omega = 1.9674147913622104;
double OMEGA = FastMath.toRadians(261);
double lv = 0;
double mu = 3.9860047e14;
inertialFrame = FramesFactory.getEME2000();
Orbit initialState = new KeplerianOrbit(a, e, i, omega, OMEGA, lv, PositionAngle.TRUE,
inertialFrame, initDate, mu);
propagator = new KeplerianPropagator(initialState);
context = new EphemerisContext();
}
@Test
public void testRangeWithBias() {
double dt = finalDate.durationFrom(initDate);
double timeStep = dt / 20.0;
List<SpacecraftState> states = new ArrayList<SpacecraftState>();
for(double t = 0 ; t <= dt; t+=timeStep) {
states.add(propagator.propagate(initDate.shiftedBy(t)));
}
final Ephemeris ephemeris = new Ephemeris(states, 3);
final double refBias = 1234.56;
final List<ObservedMeasurement<?>> measurements =
KeplerianEstimationTestUtils.createMeasurements(ephemeris,
new RangeMeasurementCreator(context, refBias),
1.0, 5.0, 10.0);
// estimated bias
final Bias<Range> rangeBias = new Bias<Range>(new String[] {"rangeBias"}, new double[] {0.0},
new double[] {1.0},
new double[] {0.0}, new double[] {10000.0});
rangeBias.getParametersDrivers().get(0).setSelected(true);
// create orbit estimator
final BatchLSEstimator estimator = new BatchLSEstimator(new LevenbergMarquardtOptimizer(),
new EphemerisPropagatorBuilder(states, 3,
ephemeris.getExtrapolationThreshold(),
ephemeris.getAttitudeProvider()));
for (final ObservedMeasurement<?> range : measurements) {
((Range) range).addModifier(rangeBias);
estimator.addMeasurement(range);
}
estimator.setParametersConvergenceThreshold(1.0e-2);
estimator.setMaxIterations(30);
estimator.setMaxEvaluations(30);
// estimate
estimator.estimate();
// verify
Assertions.assertEquals(refBias, estimator.getMeasurementsParametersDrivers(true).getDrivers().get(0).getValue(), 1.0e-7);
Assertions.assertEquals(1, estimator.getMeasurementsParametersDrivers(true).getNbParams());
Assertions.assertEquals(0, estimator.getOrbitalParametersDrivers(true).getNbParams());
Assertions.assertEquals(0, estimator.getPropagatorParametersDrivers(true).getNbParams());
}
@Test
public void testRangeRateWithClockDrift() {
double dt = finalDate.durationFrom(initDate);
double timeStep = dt / 20.0;
List<SpacecraftState> states = new ArrayList<SpacecraftState>();
for(double t = 0 ; t <= dt; t+=timeStep) {
states.add(propagator.propagate(initDate.shiftedBy(t)));
}
final Ephemeris ephemeris = new Ephemeris(states, 3);
final double refClockBias = 653.47e-11;
final RangeRateMeasurementCreator creator = new RangeRateMeasurementCreator(context, false, refClockBias);
creator.getSatellite().getClockDriftDriver().setSelected(true);
final List<ObservedMeasurement<?>> measurements =
KeplerianEstimationTestUtils.createMeasurements(ephemeris, creator,
1.0, 5.0, 10.0);
// create orbit estimator
final BatchLSEstimator estimator = new BatchLSEstimator(new LevenbergMarquardtOptimizer(),
new EphemerisPropagatorBuilder(states, 3,
ephemeris.getExtrapolationThreshold(),
ephemeris.getAttitudeProvider()));
for (final ObservedMeasurement<?> rangeRate : measurements) {
estimator.addMeasurement(rangeRate);
}
estimator.setParametersConvergenceThreshold(1.0e-2);
estimator.setMaxIterations(30);
estimator.setMaxEvaluations(30);
// estimate
estimator.estimate();
// verify
Assertions.assertEquals(refClockBias, estimator.getMeasurementsParametersDrivers(true).getDrivers().get(0).getValue(), 1.0e-17);
Assertions.assertEquals(1, estimator.getMeasurementsParametersDrivers(true).getNbParams());
Assertions.assertEquals(0, estimator.getOrbitalParametersDrivers(true).getNbParams());
Assertions.assertEquals(0, estimator.getPropagatorParametersDrivers(true).getNbParams());
}
@Test
public void testAzElWithBias() {
double dt = finalDate.durationFrom(initDate);
double timeStep = dt / 20.0;
List<SpacecraftState> states = new ArrayList<SpacecraftState>();
for(double t = 0 ; t <= dt; t+=timeStep) {
states.add(propagator.propagate(initDate.shiftedBy(t)));
}
final Ephemeris ephemeris = new Ephemeris(states, 3);
final double refAzBias = FastMath.toRadians(0.3);
final double refElBias = FastMath.toRadians(0.1);
final List<ObservedMeasurement<?>> measurements =
KeplerianEstimationTestUtils.createMeasurements(ephemeris,
new AngularAzElMeasurementCreator(context, refAzBias, refElBias),
1.0, 5.0, 10.0);
// estimated bias
final Bias<AngularAzEl> azElBias = new Bias<>(new String[] {"azBias", "elBias"}, new double[] {0.0, 0.0},
new double[] {1.0, 1.0},
new double[] {0.0, 0.0}, new double[] {2.0, 2.0});
azElBias.getParametersDrivers().get(0).setSelected(true);
azElBias.getParametersDrivers().get(1).setSelected(true);
// create orbit estimator
final BatchLSEstimator estimator = new BatchLSEstimator(new LevenbergMarquardtOptimizer(),
new EphemerisPropagatorBuilder(states, 3,
ephemeris.getExtrapolationThreshold(),
ephemeris.getAttitudeProvider()));
for (final ObservedMeasurement<?> azEl : measurements) {
((AngularAzEl) azEl).addModifier(azElBias);
estimator.addMeasurement(azEl);
}