DSSTPropagator.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.propagation.semianalytical.dsst;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Set;
- import org.hipparchus.linear.RealMatrix;
- import org.hipparchus.ode.ODEIntegrator;
- import org.hipparchus.ode.ODEStateAndDerivative;
- import org.hipparchus.ode.sampling.ODEStateInterpolator;
- import org.hipparchus.ode.sampling.ODEStepHandler;
- import org.hipparchus.util.FastMath;
- import org.hipparchus.util.MathUtils;
- import org.orekit.annotation.DefaultDataContext;
- import org.orekit.attitudes.Attitude;
- import org.orekit.attitudes.AttitudeProvider;
- import org.orekit.data.DataContext;
- import org.orekit.errors.OrekitException;
- import org.orekit.errors.OrekitMessages;
- import org.orekit.frames.Frame;
- import org.orekit.orbits.EquinoctialOrbit;
- import org.orekit.orbits.Orbit;
- import org.orekit.orbits.OrbitType;
- import org.orekit.orbits.PositionAngleType;
- import org.orekit.propagation.MatricesHarvester;
- import org.orekit.propagation.PropagationType;
- import org.orekit.propagation.Propagator;
- import org.orekit.propagation.SpacecraftState;
- import org.orekit.propagation.integration.AbstractIntegratedPropagator;
- import org.orekit.propagation.integration.AdditionalDerivativesProvider;
- import org.orekit.propagation.integration.StateMapper;
- import org.orekit.propagation.numerical.NumericalPropagator;
- import org.orekit.propagation.semianalytical.dsst.forces.DSSTForceModel;
- import org.orekit.propagation.semianalytical.dsst.forces.DSSTNewtonianAttraction;
- import org.orekit.propagation.semianalytical.dsst.forces.ShortPeriodTerms;
- import org.orekit.propagation.semianalytical.dsst.utilities.AuxiliaryElements;
- import org.orekit.propagation.semianalytical.dsst.utilities.FixedNumberInterpolationGrid;
- import org.orekit.propagation.semianalytical.dsst.utilities.InterpolationGrid;
- import org.orekit.propagation.semianalytical.dsst.utilities.MaxGapInterpolationGrid;
- import org.orekit.time.AbsoluteDate;
- import org.orekit.utils.DoubleArrayDictionary;
- import org.orekit.utils.ParameterDriver;
- import org.orekit.utils.ParameterDriversList;
- import org.orekit.utils.ParameterDriversList.DelegatingDriver;
- import org.orekit.utils.ParameterObserver;
- import org.orekit.utils.TimeSpanMap;
- import org.orekit.utils.TimeSpanMap.Span;
- /**
- * This class propagates {@link org.orekit.orbits.Orbit orbits} using the DSST theory.
- * <p>
- * Whereas analytical propagators are configured only thanks to their various
- * constructors and can be used immediately after construction, such a semianalytical
- * propagator configuration involves setting several parameters between construction
- * time and propagation time, just as numerical propagators.
- * </p>
- * <p>
- * The configuration parameters that can be set are:
- * </p>
- * <ul>
- * <li>the initial spacecraft state ({@link #setInitialState(SpacecraftState)})</li>
- * <li>the various force models ({@link #addForceModel(DSSTForceModel)},
- * {@link #removeForceModels()})</li>
- * <li>the discrete events that should be triggered during propagation (
- * {@link #addEventDetector(org.orekit.propagation.events.EventDetector)},
- * {@link #clearEventsDetectors()})</li>
- * <li>the binding logic with the rest of the application ({@link #getMultiplexer()})</li>
- * </ul>
- * <p>
- * From these configuration parameters, only the initial state is mandatory.
- * The default propagation settings are in {@link OrbitType#EQUINOCTIAL equinoctial}
- * parameters with {@link PositionAngleType#TRUE true} longitude argument.
- * The central attraction coefficient used to define the initial orbit will be used.
- * However, specifying only the initial state would mean the propagator would use
- * only Keplerian forces. In this case, the simpler
- * {@link org.orekit.propagation.analytical.KeplerianPropagator KeplerianPropagator}
- * class would be more effective.
- * </p>
- * <p>
- * The underlying numerical integrator set up in the constructor may also have
- * its own configuration parameters. Typical configuration parameters for adaptive
- * stepsize integrators are the min, max and perhaps start step size as well as
- * the absolute and/or relative errors thresholds.
- * </p>
- * <p>
- * The state that is seen by the integrator is a simple six elements double array.
- * These six elements are:
- * <ul>
- * <li>the {@link org.orekit.orbits.EquinoctialOrbit equinoctial orbit parameters}
- * (a, e<sub>x</sub>, e<sub>y</sub>, h<sub>x</sub>, h<sub>y</sub>, λ<sub>m</sub>)
- * in meters and radians,</li>
- * </ul>
- *
- * <p>By default, at the end of the propagation, the propagator resets the initial state to the final state,
- * thus allowing a new propagation to be started from there without recomputing the part already performed.
- * This behaviour can be chenged by calling {@link #setResetAtEnd(boolean)}.
- * </p>
- * <p>Beware the same instance cannot be used simultaneously by different threads, the class is <em>not</em>
- * thread-safe.</p>
- *
- * @see SpacecraftState
- * @see DSSTForceModel
- * @author Romain Di Costanzo
- * @author Pascal Parraud
- */
- public class DSSTPropagator extends AbstractIntegratedPropagator {
- /** Retrograde factor I.
- * <p>
- * DSST model needs equinoctial orbit as internal representation.
- * Classical equinoctial elements have discontinuities when inclination
- * is close to zero. In this representation, I = +1. <br>
- * To avoid this discontinuity, another representation exists and equinoctial
- * elements can be expressed in a different way, called "retrograde" orbit.
- * This implies I = -1. <br>
- * As Orekit doesn't implement the retrograde orbit, I is always set to +1.
- * But for the sake of consistency with the theory, the retrograde factor
- * has been kept in the formulas.
- * </p>
- */
- private static final int I = 1;
- /** Default value for epsilon. */
- private static final double EPSILON_DEFAULT = 1.0e-13;
- /** Default value for maxIterations. */
- private static final int MAX_ITERATIONS_DEFAULT = 200;
- /** Number of grid points per integration step to be used in interpolation of short periodics coefficients.*/
- private static final int INTERPOLATION_POINTS_PER_STEP = 3;
- /** Flag specifying whether the initial orbital state is given with osculating elements. */
- private boolean initialIsOsculating;
- /** Force models used to compute short periodic terms. */
- private final transient List<DSSTForceModel> forceModels;
- /** State mapper holding the force models. */
- private MeanPlusShortPeriodicMapper mapper;
- /** Generator for the interpolation grid. */
- private InterpolationGrid interpolationgrid;
- /**
- * Same as {@link org.orekit.propagation.AbstractPropagator#harvester} but with the
- * more specific type. Saved to avoid a cast.
- */
- private DSSTHarvester harvester;
- /** Create a new instance of DSSTPropagator.
- * <p>
- * After creation, there are no perturbing forces at all.
- * This means that if {@link #addForceModel addForceModel}
- * is not called after creation, the integrated orbit will
- * follow a Keplerian evolution only.
- * </p>
- *
- * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
- *
- * @param integrator numerical integrator to use for propagation.
- * @param propagationType type of orbit to output (mean or osculating).
- * @see #DSSTPropagator(ODEIntegrator, PropagationType, AttitudeProvider)
- */
- @DefaultDataContext
- public DSSTPropagator(final ODEIntegrator integrator, final PropagationType propagationType) {
- this(integrator, propagationType,
- Propagator.getDefaultLaw(DataContext.getDefault().getFrames()));
- }
- /** Create a new instance of DSSTPropagator.
- * <p>
- * After creation, there are no perturbing forces at all.
- * This means that if {@link #addForceModel addForceModel}
- * is not called after creation, the integrated orbit will
- * follow a Keplerian evolution only.
- * </p>
- * @param integrator numerical integrator to use for propagation.
- * @param propagationType type of orbit to output (mean or osculating).
- * @param attitudeProvider the attitude law.
- * @since 10.1
- */
- public DSSTPropagator(final ODEIntegrator integrator,
- final PropagationType propagationType,
- final AttitudeProvider attitudeProvider) {
- super(integrator, propagationType);
- forceModels = new ArrayList<>();
- initMapper();
- // DSST uses only equinoctial orbits and mean longitude argument
- setOrbitType(OrbitType.EQUINOCTIAL);
- setPositionAngleType(PositionAngleType.MEAN);
- setAttitudeProvider(attitudeProvider);
- setInterpolationGridToFixedNumberOfPoints(INTERPOLATION_POINTS_PER_STEP);
- }
- /** Create a new instance of DSSTPropagator.
- * <p>
- * After creation, there are no perturbing forces at all.
- * This means that if {@link #addForceModel addForceModel}
- * is not called after creation, the integrated orbit will
- * follow a Keplerian evolution only. Only the mean orbits
- * will be generated.
- * </p>
- *
- * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
- *
- * @param integrator numerical integrator to use for propagation.
- * @see #DSSTPropagator(ODEIntegrator, PropagationType, AttitudeProvider)
- */
- @DefaultDataContext
- public DSSTPropagator(final ODEIntegrator integrator) {
- this(integrator, PropagationType.MEAN);
- }
- /** Set the central attraction coefficient μ.
- * <p>
- * Setting the central attraction coefficient is
- * equivalent to {@link #addForceModel(DSSTForceModel) add}
- * a {@link DSSTNewtonianAttraction} force model.
- * </p>
- * @param mu central attraction coefficient (m³/s²)
- * @see #addForceModel(DSSTForceModel)
- * @see #getAllForceModels()
- */
- public void setMu(final double mu) {
- addForceModel(new DSSTNewtonianAttraction(mu));
- }
- /** Set the central attraction coefficient μ only in upper class.
- * @param mu central attraction coefficient (m³/s²)
- */
- private void superSetMu(final double mu) {
- super.setMu(mu);
- }
- /** Check if Newtonian attraction force model is available.
- * <p>
- * Newtonian attraction is always the last force model in the list.
- * </p>
- * @return true if Newtonian attraction force model is available
- */
- private boolean hasNewtonianAttraction() {
- final int last = forceModels.size() - 1;
- return last >= 0 && forceModels.get(last) instanceof DSSTNewtonianAttraction;
- }
- /** Set the initial state with osculating orbital elements.
- * @param initialState initial state (defined with osculating elements)
- */
- public void setInitialState(final SpacecraftState initialState) {
- setInitialState(initialState, PropagationType.OSCULATING);
- }
- /** Set the initial state.
- * @param initialState initial state
- * @param stateType defined if the orbital state is defined with osculating or mean elements
- */
- public void setInitialState(final SpacecraftState initialState,
- final PropagationType stateType) {
- resetInitialState(initialState, stateType);
- }
- /** Reset the initial state.
- *
- * @param state new initial state
- */
- @Override
- public void resetInitialState(final SpacecraftState state) {
- super.resetInitialState(state);
- if (!hasNewtonianAttraction()) {
- // use the state to define central attraction
- setMu(state.getMu());
- }
- super.setStartDate(state.getDate());
- }
- /** {@inheritDoc}.
- *
- * <p>Change parameter {@link #initialIsOsculating()} accordingly
- * @since 12.1.3
- */
- @Override
- public void resetInitialState(final SpacecraftState state, final PropagationType stateType) {
- // Reset initial state
- resetInitialState(state);
- // Change state of initial osculating, if needed
- initialIsOsculating = stateType == PropagationType.OSCULATING;
- }
- /** Set the selected short periodic coefficients that must be stored as additional states.
- * @param selectedCoefficients short periodic coefficients that must be stored as additional states
- * (null means no coefficients are selected, empty set means all coefficients are selected)
- */
- public void setSelectedCoefficients(final Set<String> selectedCoefficients) {
- mapper.setSelectedCoefficients(selectedCoefficients == null ? null : new HashSet<>(selectedCoefficients));
- }
- /** Get the selected short periodic coefficients that must be stored as additional states.
- * @return short periodic coefficients that must be stored as additional states
- * (null means no coefficients are selected, empty set means all coefficients are selected)
- */
- public Set<String> getSelectedCoefficients() {
- final Set<String> set = mapper.getSelectedCoefficients();
- return set == null ? null : Collections.unmodifiableSet(set);
- }
- /** Get the names of the parameters in the matrix returned by {@link MatricesHarvester#getParametersJacobian}.
- * @return names of the parameters (i.e. columns) of the Jacobian matrix
- */
- protected List<String> getJacobiansColumnsNames() {
- final List<String> columnsNames = new ArrayList<>();
- for (final DSSTForceModel forceModel : getAllForceModels()) {
- for (final ParameterDriver driver : forceModel.getParametersDrivers()) {
- if (driver.isSelected() && !columnsNames.contains(driver.getNamesSpanMap().getFirstSpan().getData())) {
- // As driver with same name should have same NamesSpanMap we only check if the first span is present,
- // if not we add all span names to columnsNames
- for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
- columnsNames.add(span.getData());
- }
- }
- }
- }
- Collections.sort(columnsNames);
- return columnsNames;
- }
- /** {@inheritDoc} */
- @Override
- public DSSTHarvester setupMatricesComputation(
- final String stmName,
- final RealMatrix initialStm,
- final DoubleArrayDictionary initialJacobianColumns) {
- if (stmName == null) {
- throw new OrekitException(OrekitMessages.NULL_ARGUMENT, "stmName");
- }
- final DSSTHarvester dsstHarvester =
- createHarvester(stmName, initialStm, initialJacobianColumns);
- return this.harvester = dsstHarvester;
- }
- /** {@inheritDoc} */
- @Override
- protected DSSTHarvester createHarvester(final String stmName, final RealMatrix initialStm,
- final DoubleArrayDictionary initialJacobianColumns) {
- final DSSTHarvester dsstHarvester =
- new DSSTHarvester(this, stmName, initialStm, initialJacobianColumns);
- this.harvester = dsstHarvester;
- return dsstHarvester;
- }
- /** {@inheritDoc} */
- @Override
- protected DSSTHarvester getHarvester() {
- return harvester;
- }
- /** {@inheritDoc} */
- @Override
- protected void setUpStmAndJacobianGenerators() {
- final DSSTHarvester dsstHarvester = getHarvester();
- if (dsstHarvester != null) {
- // set up the additional equations and additional state providers
- final DSSTStateTransitionMatrixGenerator stmGenerator = setUpStmGenerator();
- setUpRegularParametersJacobiansColumns(stmGenerator);
- // as we are now starting the propagation, everything is configured
- // we can freeze the names in the harvester
- dsstHarvester.freezeColumnsNames();
- }
- }
- /** Set up the State Transition Matrix Generator.
- * @return State Transition Matrix Generator
- * @since 11.1
- */
- private DSSTStateTransitionMatrixGenerator setUpStmGenerator() {
- final DSSTHarvester dsstHarvester = getHarvester();
- // add the STM generator corresponding to the current settings, and setup state accordingly
- DSSTStateTransitionMatrixGenerator stmGenerator = null;
- for (final AdditionalDerivativesProvider equations : getAdditionalDerivativesProviders()) {
- if (equations instanceof DSSTStateTransitionMatrixGenerator &&
- equations.getName().equals(dsstHarvester.getStmName())) {
- // the STM generator has already been set up in a previous propagation
- stmGenerator = (DSSTStateTransitionMatrixGenerator) equations;
- break;
- }
- }
- if (stmGenerator == null) {
- // this is the first time we need the STM generate, create it
- stmGenerator = new DSSTStateTransitionMatrixGenerator(dsstHarvester.getStmName(),
- getAllForceModels(),
- getAttitudeProvider(),
- getPropagationType());
- addAdditionalDerivativesProvider(stmGenerator);
- }
- if (!getInitialIntegrationState().hasAdditionalState(dsstHarvester.getStmName())) {
- // add the initial State Transition Matrix if it is not already there
- // (perhaps due to a previous propagation)
- setInitialState(stmGenerator.setInitialStateTransitionMatrix(getInitialState(),
- dsstHarvester.getInitialStateTransitionMatrix()),
- getPropagationType());
- }
- return stmGenerator;
- }
- /** Set up the Jacobians columns generator for regular parameters.
- * @param stmGenerator generator for the State Transition Matrix
- * @since 11.1
- */
- private void setUpRegularParametersJacobiansColumns(final DSSTStateTransitionMatrixGenerator stmGenerator) {
- // first pass: gather all parameters (excluding trigger dates), binding similar names together
- final ParameterDriversList selected = new ParameterDriversList();
- for (final DSSTForceModel forceModel : getAllForceModels()) {
- for (final ParameterDriver driver : forceModel.getParametersDrivers()) {
- selected.add(driver);
- }
- }
- // second pass: now that shared parameter names are bound together,
- // their selections status have been synchronized, we can filter them
- selected.filter(true);
- // third pass: sort parameters lexicographically
- selected.sort();
- // add the Jacobians column generators corresponding to parameters, and setup state accordingly
- for (final DelegatingDriver driver : selected.getDrivers()) {
- for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
- DSSTIntegrableJacobianColumnGenerator generator = null;
- // check if we already have set up the providers
- for (final AdditionalDerivativesProvider provider : getAdditionalDerivativesProviders()) {
- if (provider instanceof DSSTIntegrableJacobianColumnGenerator &&
- provider.getName().equals(span.getData())) {
- // the Jacobian column generator has already been set up in a previous propagation
- generator = (DSSTIntegrableJacobianColumnGenerator) provider;
- break;
- }
- }
- if (generator == null) {
- // this is the first time we need the Jacobian column generator, create it
- generator = new DSSTIntegrableJacobianColumnGenerator(stmGenerator, span.getData());
- addAdditionalDerivativesProvider(generator);
- }
- if (!getInitialIntegrationState().hasAdditionalState(span.getData())) {
- // add the initial Jacobian column if it is not already there
- // (perhaps due to a previous propagation)
- setInitialState(getInitialState().addAdditionalState(span.getData(),
- getHarvester().getInitialJacobianColumn(span.getData())),
- getPropagationType());
- }
- }
- }
- }
- /** Check if the initial state is provided in osculating elements.
- * @return true if initial state is provided in osculating elements
- */
- public boolean initialIsOsculating() {
- return initialIsOsculating;
- }
- /** Set the interpolation grid generator.
- * <p>
- * The generator will create an interpolation grid with a fixed
- * number of points for each mean element integration step.
- * </p>
- * <p>
- * If neither {@link #setInterpolationGridToFixedNumberOfPoints(int)}
- * nor {@link #setInterpolationGridToMaxTimeGap(double)} has been called,
- * by default the propagator is set as to 3 interpolations points per step.
- * </p>
- * @param interpolationPoints number of interpolation points at
- * each integration step
- * @see #setInterpolationGridToMaxTimeGap(double)
- * @since 7.1
- */
- public void setInterpolationGridToFixedNumberOfPoints(final int interpolationPoints) {
- interpolationgrid = new FixedNumberInterpolationGrid(interpolationPoints);
- }
- /** Set the interpolation grid generator.
- * <p>
- * The generator will create an interpolation grid with a maximum
- * time gap between interpolation points.
- * </p>
- * <p>
- * If neither {@link #setInterpolationGridToFixedNumberOfPoints(int)}
- * nor {@link #setInterpolationGridToMaxTimeGap(double)} has been called,
- * by default the propagator is set as to 3 interpolations points per step.
- * </p>
- * @param maxGap maximum time gap between interpolation points (seconds)
- * @see #setInterpolationGridToFixedNumberOfPoints(int)
- * @since 7.1
- */
- public void setInterpolationGridToMaxTimeGap(final double maxGap) {
- interpolationgrid = new MaxGapInterpolationGrid(maxGap);
- }
- /** Add a force model to the global perturbation model.
- * <p>
- * If this method is not called at all,
- * the integrated orbit will follow a Keplerian evolution only.
- * </p>
- * @param force perturbing {@link DSSTForceModel force} to add
- * @see #removeForceModels()
- * @see #setMu(double)
- */
- public void addForceModel(final DSSTForceModel force) {
- if (force instanceof DSSTNewtonianAttraction) {
- // we want to add the central attraction force model
- // ensure we are notified of any mu change
- force.getParametersDrivers().get(0).addObserver(new ParameterObserver() {
- /** {@inheritDoc} */
- @Override
- public void valueChanged(final double previousValue, final ParameterDriver driver, final AbsoluteDate date) {
- // mu PDriver should have only 1 span
- superSetMu(driver.getValue());
- }
- /** {@inheritDoc} */
- @Override
- public void valueSpanMapChanged(final TimeSpanMap<Double> previousValue, final ParameterDriver driver) {
- // mu PDriver should have only 1 span
- superSetMu(driver.getValue());
- }
- });
- if (hasNewtonianAttraction()) {
- // there is already a central attraction model, replace it
- forceModels.set(forceModels.size() - 1, force);
- } else {
- // there are no central attraction model yet, add it at the end of the list
- forceModels.add(force);
- }
- } else {
- // we want to add a perturbing force model
- if (hasNewtonianAttraction()) {
- // insert the new force model before Newtonian attraction,
- // which should always be the last one in the list
- forceModels.add(forceModels.size() - 1, force);
- } else {
- // we only have perturbing force models up to now, just append at the end of the list
- forceModels.add(force);
- }
- }
- force.registerAttitudeProvider(getAttitudeProvider());
- }
- /** Remove all perturbing force models from the global perturbation model
- * (except central attraction).
- * <p>
- * Once all perturbing forces have been removed (and as long as no new force model is added),
- * the integrated orbit will follow a Keplerian evolution only.
- * </p>
- * @see #addForceModel(DSSTForceModel)
- */
- public void removeForceModels() {
- final int last = forceModels.size() - 1;
- if (hasNewtonianAttraction()) {
- // preserve the Newtonian attraction model at the end
- final DSSTForceModel newton = forceModels.get(last);
- forceModels.clear();
- forceModels.add(newton);
- } else {
- forceModels.clear();
- }
- }
- /** Get all the force models, perturbing forces and Newtonian attraction included.
- * @return list of perturbing force models, with Newtonian attraction being the
- * last one
- * @see #addForceModel(DSSTForceModel)
- * @see #setMu(double)
- */
- public List<DSSTForceModel> getAllForceModels() {
- return Collections.unmodifiableList(forceModels);
- }
- /** Get propagation parameter type.
- * @return orbit type used for propagation
- */
- public OrbitType getOrbitType() {
- return super.getOrbitType();
- }
- /** Get propagation parameter type.
- * @return angle type to use for propagation
- */
- public PositionAngleType getPositionAngleType() {
- return super.getPositionAngleType();
- }
- /** Conversion from mean to osculating orbit.
- * <p>
- * Compute osculating state <b>in a DSST sense</b>, corresponding to the
- * mean SpacecraftState in input, and according to the Force models taken
- * into account.
- * </p><p>
- * Since the osculating state is obtained by adding short-periodic variation
- * of each force model, the resulting output will depend on the
- * force models parameterized in input.
- * </p>
- * @param mean Mean state to convert
- * @param forces Forces to take into account
- * @param attitudeProvider attitude provider (may be null if there are no Gaussian force models
- * like atmospheric drag, radiation pressure or specific user-defined models)
- * @return osculating state in a DSST sense
- */
- public static SpacecraftState computeOsculatingState(final SpacecraftState mean,
- final AttitudeProvider attitudeProvider,
- final Collection<DSSTForceModel> forces) {
- //Create the auxiliary object
- final AuxiliaryElements aux = new AuxiliaryElements(mean.getOrbit(), I);
- // Set the force models
- final List<ShortPeriodTerms> shortPeriodTerms = new ArrayList<>();
- for (final DSSTForceModel force : forces) {
- force.registerAttitudeProvider(attitudeProvider);
- shortPeriodTerms.addAll(force.initializeShortPeriodTerms(aux, PropagationType.OSCULATING, force.getParameters(mean.getDate())));
- force.updateShortPeriodTerms(force.getParametersAllValues(), mean);
- }
- final EquinoctialOrbit osculatingOrbit = computeOsculatingOrbit(mean, shortPeriodTerms);
- return new SpacecraftState(osculatingOrbit, mean.getAttitude(), mean.getMass(),
- mean.getAdditionalStatesValues(), mean.getAdditionalStatesDerivatives());
- }
- /** Conversion from osculating to mean orbit.
- * <p>
- * Compute mean state <b>in a DSST sense</b>, corresponding to the
- * osculating SpacecraftState in input, and according to the Force models
- * taken into account.
- * </p><p>
- * Since the osculating state is obtained with the computation of
- * short-periodic variation of each force model, the resulting output will
- * depend on the force models parameterized in input.
- * </p><p>
- * The computation is done through a fixed-point iteration process.
- * </p>
- * @param osculating Osculating state to convert
- * @param attitudeProvider attitude provider (may be null if there are no Gaussian force models
- * like atmospheric drag, radiation pressure or specific user-defined models)
- * @param forceModels Forces to take into account
- * @return mean state in a DSST sense
- */
- public static SpacecraftState computeMeanState(final SpacecraftState osculating,
- final AttitudeProvider attitudeProvider,
- final Collection<DSSTForceModel> forceModels) {
- return computeMeanState(osculating, attitudeProvider, forceModels, EPSILON_DEFAULT, MAX_ITERATIONS_DEFAULT);
- }
- /** Conversion from osculating to mean orbit.
- * <p>
- * Compute mean state <b>in a DSST sense</b>, corresponding to the
- * osculating SpacecraftState in input, and according to the Force models
- * taken into account.
- * </p><p>
- * Since the osculating state is obtained with the computation of
- * short-periodic variation of each force model, the resulting output will
- * depend on the force models parameterized in input.
- * </p><p>
- * The computation is done through a fixed-point iteration process.
- * </p>
- * @param osculating Osculating state to convert
- * @param attitudeProvider attitude provider (may be null if there are no Gaussian force models
- * like atmospheric drag, radiation pressure or specific user-defined models)
- * @param forceModels Forces to take into account
- * @param epsilon convergence threshold for mean parameters conversion
- * @param maxIterations maximum iterations for mean parameters conversion
- * @return mean state in a DSST sense
- * @since 10.1
- */
- public static SpacecraftState computeMeanState(final SpacecraftState osculating,
- final AttitudeProvider attitudeProvider,
- final Collection<DSSTForceModel> forceModels,
- final double epsilon,
- final int maxIterations) {
- final Orbit meanOrbit = computeMeanOrbit(osculating, attitudeProvider, forceModels, epsilon, maxIterations);
- return new SpacecraftState(meanOrbit, osculating.getAttitude(), osculating.getMass(),
- osculating.getAdditionalStatesValues(), osculating.getAdditionalStatesDerivatives());
- }
- /** Override the default value of the parameter.
- * <p>
- * By default, if the initial orbit is defined as osculating,
- * it will be averaged over 2 satellite revolutions.
- * This can be changed by using this method.
- * </p>
- * @param satelliteRevolution number of satellite revolutions to use for converting osculating to mean
- * elements
- */
- public void setSatelliteRevolution(final int satelliteRevolution) {
- mapper.setSatelliteRevolution(satelliteRevolution);
- }
- /** Get the number of satellite revolutions to use for converting osculating to mean elements.
- * @return number of satellite revolutions to use for converting osculating to mean elements
- */
- public int getSatelliteRevolution() {
- return mapper.getSatelliteRevolution();
- }
- /** Override the default value short periodic terms.
- * <p>
- * By default, short periodic terms are initialized before
- * the numerical integration of the mean orbital elements.
- * </p>
- * @param shortPeriodTerms short periodic terms
- */
- public void setShortPeriodTerms(final List<ShortPeriodTerms> shortPeriodTerms) {
- mapper.setShortPeriodTerms(shortPeriodTerms);
- }
- /** Get the short periodic terms.
- * @return the short periodic terms
- */
- public List<ShortPeriodTerms> getShortPeriodTerms() {
- return mapper.getShortPeriodTerms();
- }
- /** {@inheritDoc} */
- @Override
- public void setAttitudeProvider(final AttitudeProvider attitudeProvider) {
- super.setAttitudeProvider(attitudeProvider);
- //Register the attitude provider for each force model
- for (final DSSTForceModel force : forceModels) {
- force.registerAttitudeProvider(attitudeProvider);
- }
- }
- /** Method called just before integration.
- * <p>
- * The default implementation does nothing, it may be specialized in subclasses.
- * </p>
- * @param initialState initial state
- * @param tEnd target date at which state should be propagated
- */
- @Override
- protected void beforeIntegration(final SpacecraftState initialState,
- final AbsoluteDate tEnd) {
- // If this method is updated also update DSSTStateTransitionMatrixGenerator.init(...)
- // check if only mean elements must be used
- final PropagationType type = getPropagationType();
- // compute common auxiliary elements
- final AuxiliaryElements aux = new AuxiliaryElements(initialState.getOrbit(), I);
- // initialize all perturbing forces
- final List<ShortPeriodTerms> shortPeriodTerms = new ArrayList<>();
- for (final DSSTForceModel force : forceModels) {
- shortPeriodTerms.addAll(force.initializeShortPeriodTerms(aux, type, force.getParameters(initialState.getDate())));
- }
- mapper.setShortPeriodTerms(shortPeriodTerms);
- // if required, insert the special short periodics step handler
- if (type == PropagationType.OSCULATING) {
- final ShortPeriodicsHandler spHandler = new ShortPeriodicsHandler(forceModels);
- // Compute short periodic coefficients for this point
- for (DSSTForceModel forceModel : forceModels) {
- forceModel.updateShortPeriodTerms(forceModel.getParametersAllValues(), initialState);
- }
- final Collection<ODEStepHandler> stepHandlers = new ArrayList<>();
- stepHandlers.add(spHandler);
- final ODEIntegrator integrator = getIntegrator();
- final Collection<ODEStepHandler> existing = integrator.getStepHandlers();
- stepHandlers.addAll(existing);
- integrator.clearStepHandlers();
- // add back the existing handlers after the short periodics one
- for (final ODEStepHandler sp : stepHandlers) {
- integrator.addStepHandler(sp);
- }
- }
- }
- /** {@inheritDoc} */
- @Override
- protected void afterIntegration() {
- // remove the special short periodics step handler if added before
- if (getPropagationType() == PropagationType.OSCULATING) {
- final List<ODEStepHandler> preserved = new ArrayList<>();
- final ODEIntegrator integrator = getIntegrator();
- for (final ODEStepHandler sp : integrator.getStepHandlers()) {
- if (!(sp instanceof ShortPeriodicsHandler)) {
- preserved.add(sp);
- }
- }
- // clear the list
- integrator.clearStepHandlers();
- // add back the step handlers that were important for the user
- for (final ODEStepHandler sp : preserved) {
- integrator.addStepHandler(sp);
- }
- }
- }
- /** Compute mean state from osculating state.
- * <p>
- * Compute in a DSST sense the mean state corresponding to the input osculating state.
- * </p><p>
- * The computing is done through a fixed-point iteration process.
- * </p>
- * @param osculating initial osculating state
- * @param attitudeProvider attitude provider (may be null if there are no Gaussian force models
- * like atmospheric drag, radiation pressure or specific user-defined models)
- * @param forceModels force models
- * @param epsilon convergence threshold for mean parameters conversion
- * @param maxIterations maximum iterations for mean parameters conversion
- * @return mean state
- */
- private static Orbit computeMeanOrbit(final SpacecraftState osculating,
- final AttitudeProvider attitudeProvider,
- final Collection<DSSTForceModel> forceModels, final double epsilon, final int maxIterations) {
- // rough initialization of the mean parameters
- EquinoctialOrbit meanOrbit = (EquinoctialOrbit) OrbitType.EQUINOCTIAL.convertType(osculating.getOrbit());
- // threshold for each parameter
- final double thresholdA = epsilon * (1 + FastMath.abs(meanOrbit.getA()));
- final double thresholdE = epsilon * (1 + meanOrbit.getE());
- final double thresholdI = epsilon * (1 + meanOrbit.getI());
- final double thresholdL = epsilon * FastMath.PI;
- // ensure all Gaussian force models can rely on attitude
- for (final DSSTForceModel force : forceModels) {
- force.registerAttitudeProvider(attitudeProvider);
- }
- int i = 0;
- while (i++ < maxIterations) {
- final SpacecraftState meanState = new SpacecraftState(meanOrbit, osculating.getAttitude(), osculating.getMass());
- //Create the auxiliary object
- final AuxiliaryElements aux = new AuxiliaryElements(meanOrbit, I);
- // Set the force models
- final List<ShortPeriodTerms> shortPeriodTerms = new ArrayList<>();
- for (final DSSTForceModel force : forceModels) {
- shortPeriodTerms.addAll(force.initializeShortPeriodTerms(aux, PropagationType.OSCULATING, force.getParameters(meanState.getDate())));
- force.updateShortPeriodTerms(force.getParametersAllValues(), meanState);
- }
- // recompute the osculating parameters from the current mean parameters
- final EquinoctialOrbit rebuilt = computeOsculatingOrbit(meanState, shortPeriodTerms);
- // adapted parameters residuals
- final double deltaA = osculating.getA() - rebuilt.getA();
- final double deltaEx = osculating.getEquinoctialEx() - rebuilt.getEquinoctialEx();
- final double deltaEy = osculating.getEquinoctialEy() - rebuilt.getEquinoctialEy();
- final double deltaHx = osculating.getHx() - rebuilt.getHx();
- final double deltaHy = osculating.getHy() - rebuilt.getHy();
- final double deltaLM = MathUtils.normalizeAngle(osculating.getLM() - rebuilt.getLM(), 0.0);
- // check convergence
- if (FastMath.abs(deltaA) < thresholdA &&
- FastMath.abs(deltaEx) < thresholdE &&
- FastMath.abs(deltaEy) < thresholdE &&
- FastMath.abs(deltaHx) < thresholdI &&
- FastMath.abs(deltaHy) < thresholdI &&
- FastMath.abs(deltaLM) < thresholdL) {
- return meanOrbit;
- }
- // update mean parameters
- meanOrbit = new EquinoctialOrbit(meanOrbit.getA() + deltaA,
- meanOrbit.getEquinoctialEx() + deltaEx,
- meanOrbit.getEquinoctialEy() + deltaEy,
- meanOrbit.getHx() + deltaHx,
- meanOrbit.getHy() + deltaHy,
- meanOrbit.getLM() + deltaLM,
- PositionAngleType.MEAN, meanOrbit.getFrame(),
- meanOrbit.getDate(), meanOrbit.getMu());
- }
- throw new OrekitException(OrekitMessages.UNABLE_TO_COMPUTE_DSST_MEAN_PARAMETERS, i);
- }
- /** Compute osculating state from mean state.
- * <p>
- * Compute and add the short periodic variation to the mean {@link SpacecraftState}.
- * </p>
- * @param meanState initial mean state
- * @param shortPeriodTerms short period terms
- * @return osculating state
- */
- private static EquinoctialOrbit computeOsculatingOrbit(final SpacecraftState meanState,
- final List<ShortPeriodTerms> shortPeriodTerms) {
- final double[] mean = new double[6];
- final double[] meanDot = new double[6];
- OrbitType.EQUINOCTIAL.mapOrbitToArray(meanState.getOrbit(), PositionAngleType.MEAN, mean, meanDot);
- final double[] y = mean.clone();
- for (final ShortPeriodTerms spt : shortPeriodTerms) {
- final double[] shortPeriodic = spt.value(meanState.getOrbit());
- for (int i = 0; i < shortPeriodic.length; i++) {
- y[i] += shortPeriodic[i];
- }
- }
- return (EquinoctialOrbit) OrbitType.EQUINOCTIAL.mapArrayToOrbit(y, meanDot,
- PositionAngleType.MEAN, meanState.getDate(),
- meanState.getMu(), meanState.getFrame());
- }
- /** {@inheritDoc} */
- @Override
- protected SpacecraftState getInitialIntegrationState() {
- if (initialIsOsculating) {
- // the initial state is an osculating state,
- // it must be converted to mean state
- return computeMeanState(getInitialState(), getAttitudeProvider(), forceModels);
- } else {
- // the initial state is already a mean state
- return getInitialState();
- }
- }
- /** {@inheritDoc}
- * <p>
- * Note that for DSST, orbit type is hardcoded to {@link OrbitType#EQUINOCTIAL}
- * and position angle type is hardcoded to {@link PositionAngleType#MEAN}, so
- * the corresponding parameters are ignored.
- * </p>
- */
- @Override
- protected StateMapper createMapper(final AbsoluteDate referenceDate, final double mu,
- final OrbitType ignoredOrbitType, final PositionAngleType ignoredPositionAngleType,
- final AttitudeProvider attitudeProvider, final Frame frame) {
- // create a mapper with the common settings provided as arguments
- final MeanPlusShortPeriodicMapper newMapper =
- new MeanPlusShortPeriodicMapper(referenceDate, mu, attitudeProvider, frame);
- // copy the specific settings from the existing mapper
- if (mapper != null) {
- newMapper.setSatelliteRevolution(mapper.getSatelliteRevolution());
- newMapper.setSelectedCoefficients(mapper.getSelectedCoefficients());
- newMapper.setShortPeriodTerms(mapper.getShortPeriodTerms());
- }
- mapper = newMapper;
- return mapper;
- }
- /** Get the short period terms value.
- * @param meanState the mean state
- * @return shortPeriodTerms short period terms
- * @since 7.1
- */
- public double[] getShortPeriodTermsValue(final SpacecraftState meanState) {
- final double[] sptValue = new double[6];
- for (ShortPeriodTerms spt : mapper.getShortPeriodTerms()) {
- final double[] shortPeriodic = spt.value(meanState.getOrbit());
- for (int i = 0; i < shortPeriodic.length; i++) {
- sptValue[i] += shortPeriodic[i];
- }
- }
- return sptValue;
- }
- /** Internal mapper using mean parameters plus short periodic terms. */
- private static class MeanPlusShortPeriodicMapper extends StateMapper {
- /** Short periodic coefficients that must be stored as additional states. */
- private Set<String> selectedCoefficients;
- /** Number of satellite revolutions in the averaging interval. */
- private int satelliteRevolution;
- /** Short period terms. */
- private List<ShortPeriodTerms> shortPeriodTerms;
- /** Simple constructor.
- * @param referenceDate reference date
- * @param mu central attraction coefficient (m³/s²)
- * @param attitudeProvider attitude provider
- * @param frame inertial frame
- */
- MeanPlusShortPeriodicMapper(final AbsoluteDate referenceDate, final double mu,
- final AttitudeProvider attitudeProvider, final Frame frame) {
- super(referenceDate, mu, OrbitType.EQUINOCTIAL, PositionAngleType.MEAN, attitudeProvider, frame);
- this.selectedCoefficients = null;
- // Default averaging period for conversion from osculating to mean elements
- this.satelliteRevolution = 2;
- this.shortPeriodTerms = Collections.emptyList();
- }
- /** {@inheritDoc} */
- @Override
- public SpacecraftState mapArrayToState(final AbsoluteDate date,
- final double[] y, final double[] yDot,
- final PropagationType type) {
- // add short periodic variations to mean elements to get osculating elements
- // (the loop may not be performed if there are no force models and in the
- // case we want to remain in mean parameters only)
- final double[] elements = y.clone();
- final DoubleArrayDictionary coefficients;
- if (type == PropagationType.MEAN) {
- coefficients = null;
- } else {
- final Orbit meanOrbit = OrbitType.EQUINOCTIAL.mapArrayToOrbit(elements, yDot, PositionAngleType.MEAN, date, getMu(), getFrame());
- coefficients = selectedCoefficients == null ? null : new DoubleArrayDictionary();
- for (final ShortPeriodTerms spt : shortPeriodTerms) {
- final double[] shortPeriodic = spt.value(meanOrbit);
- for (int i = 0; i < shortPeriodic.length; i++) {
- elements[i] += shortPeriodic[i];
- }
- if (selectedCoefficients != null) {
- coefficients.putAll(spt.getCoefficients(date, selectedCoefficients));
- }
- }
- }
- final double mass = elements[6];
- if (mass <= 0.0) {
- throw new OrekitException(OrekitMessages.NOT_POSITIVE_SPACECRAFT_MASS, mass);
- }
- final Orbit orbit = OrbitType.EQUINOCTIAL.mapArrayToOrbit(elements, yDot, PositionAngleType.MEAN, date, getMu(), getFrame());
- final Attitude attitude = getAttitudeProvider().getAttitude(orbit, date, getFrame());
- if (coefficients == null) {
- return new SpacecraftState(orbit, attitude, mass);
- } else {
- return new SpacecraftState(orbit, attitude, mass, coefficients);
- }
- }
- /** {@inheritDoc} */
- @Override
- public void mapStateToArray(final SpacecraftState state, final double[] y, final double[] yDot) {
- OrbitType.EQUINOCTIAL.mapOrbitToArray(state.getOrbit(), PositionAngleType.MEAN, y, yDot);
- y[6] = state.getMass();
- }
- /** Set the number of satellite revolutions to use for converting osculating to mean elements.
- * <p>
- * By default, if the initial orbit is defined as osculating,
- * it will be averaged over 2 satellite revolutions.
- * This can be changed by using this method.
- * </p>
- * @param satelliteRevolution number of satellite revolutions to use for converting osculating to mean
- * elements
- */
- public void setSatelliteRevolution(final int satelliteRevolution) {
- this.satelliteRevolution = satelliteRevolution;
- }
- /** Get the number of satellite revolutions to use for converting osculating to mean elements.
- * @return number of satellite revolutions to use for converting osculating to mean elements
- */
- public int getSatelliteRevolution() {
- return satelliteRevolution;
- }
- /** Set the selected short periodic coefficients that must be stored as additional states.
- * @param selectedCoefficients short periodic coefficients that must be stored as additional states
- * (null means no coefficients are selected, empty set means all coefficients are selected)
- */
- public void setSelectedCoefficients(final Set<String> selectedCoefficients) {
- this.selectedCoefficients = selectedCoefficients;
- }
- /** Get the selected short periodic coefficients that must be stored as additional states.
- * @return short periodic coefficients that must be stored as additional states
- * (null means no coefficients are selected, empty set means all coefficients are selected)
- */
- public Set<String> getSelectedCoefficients() {
- return selectedCoefficients;
- }
- /** Set the short period terms.
- * @param shortPeriodTerms short period terms
- * @since 7.1
- */
- public void setShortPeriodTerms(final List<ShortPeriodTerms> shortPeriodTerms) {
- this.shortPeriodTerms = shortPeriodTerms;
- }
- /** Get the short period terms.
- * @return shortPeriodTerms short period terms
- * @since 7.1
- */
- public List<ShortPeriodTerms> getShortPeriodTerms() {
- return shortPeriodTerms;
- }
- }
- /** {@inheritDoc} */
- @Override
- protected MainStateEquations getMainStateEquations(final ODEIntegrator integrator) {
- return new Main(integrator);
- }
- /** Internal class for mean parameters integration. */
- private class Main implements MainStateEquations {
- /** Derivatives array. */
- private final double[] yDot;
- /** Simple constructor.
- * @param integrator numerical integrator to use for propagation.
- */
- Main(final ODEIntegrator integrator) {
- yDot = new double[7];
- // Setup event detectors for each force model
- forceModels.forEach(dsstForceModel -> dsstForceModel.getEventDetectors().
- forEach(eventDetector -> setUpEventDetector(integrator, eventDetector)));
- }
- /** {@inheritDoc} */
- @Override
- public void init(final SpacecraftState initialState, final AbsoluteDate target) {
- forceModels.forEach(fm -> fm.init(initialState, target));
- }
- /** {@inheritDoc} */
- @Override
- public double[] computeDerivatives(final SpacecraftState state) {
- Arrays.fill(yDot, 0.0);
- // compute common auxiliary elements
- final AuxiliaryElements auxiliaryElements = new AuxiliaryElements(state.getOrbit(), I);
- // compute the contributions of all perturbing forces
- for (final DSSTForceModel forceModel : forceModels) {
- final double[] daidt = elementRates(forceModel, state, auxiliaryElements, forceModel.getParameters(state.getDate()));
- for (int i = 0; i < daidt.length; i++) {
- yDot[i] += daidt[i];
- }
- }
- return yDot.clone();
- }
- /** This method allows to compute the mean equinoctial elements rates da<sub>i</sub> / dt
- * for a specific force model.
- * @param forceModel force to take into account
- * @param state current state
- * @param auxiliaryElements auxiliary elements related to the current orbit
- * @param parameters force model parameters at state date (only 1 value for
- * each parameter
- * @return the mean equinoctial elements rates da<sub>i</sub> / dt
- */
- private double[] elementRates(final DSSTForceModel forceModel,
- final SpacecraftState state,
- final AuxiliaryElements auxiliaryElements,
- final double[] parameters) {
- return forceModel.getMeanElementRate(state, auxiliaryElements, parameters);
- }
- }
- /** Estimate tolerance vectors for an AdaptativeStepsizeIntegrator.
- * <p>
- * The errors are estimated from partial derivatives properties of orbits,
- * starting from a scalar position error specified by the user.
- * Considering the energy conservation equation V = sqrt(mu (2/r - 1/a)),
- * we get at constant energy (i.e. on a Keplerian trajectory):
- *
- * <pre>
- * V r² |dV| = mu |dr|
- * </pre>
- *
- * <p> So we deduce a scalar velocity error consistent with the position error. From here, we apply
- * orbits Jacobians matrices to get consistent errors on orbital parameters.
- *
- * <p>
- * The tolerances are only <em>orders of magnitude</em>, and integrator tolerances are only
- * local estimates, not global ones. So some care must be taken when using these tolerances.
- * Setting 1mm as a position error does NOT mean the tolerances will guarantee a 1mm error
- * position after several orbits integration.
- * </p>
- *
- * @param dP user specified position error (m)
- * @param orbit reference orbit
- * @return a two rows array, row 0 being the absolute tolerance error
- * and row 1 being the relative tolerance error
- */
- public static double[][] tolerances(final double dP, final Orbit orbit) {
- return NumericalPropagator.tolerances(dP, orbit, OrbitType.EQUINOCTIAL);
- }
- /** Estimate tolerance vectors for an AdaptativeStepsizeIntegrator.
- * <p>
- * The errors are estimated from partial derivatives properties of orbits,
- * starting from scalar position and velocity errors specified by the user.
- * <p>
- * The tolerances are only <em>orders of magnitude</em>, and integrator tolerances are only
- * local estimates, not global ones. So some care must be taken when using these tolerances.
- * Setting 1mm as a position error does NOT mean the tolerances will guarantee a 1mm error
- * position after several orbits integration.
- * </p>
- *
- * @param dP user specified position error (m)
- * @param dV user specified velocity error (m/s)
- * @param orbit reference orbit
- * @return a two rows array, row 0 being the absolute tolerance error
- * and row 1 being the relative tolerance error
- * @since 10.3
- */
- public static double[][] tolerances(final double dP, final double dV, final Orbit orbit) {
- return NumericalPropagator.tolerances(dP, dV, orbit, OrbitType.EQUINOCTIAL);
- }
- /** Step handler used to compute the parameters for the short periodic contributions.
- * @author Lucian Barbulescu
- */
- private class ShortPeriodicsHandler implements ODEStepHandler {
- /** Force models used to compute short periodic terms. */
- private final List<DSSTForceModel> forceModels;
- /** Constructor.
- * @param forceModels force models
- */
- ShortPeriodicsHandler(final List<DSSTForceModel> forceModels) {
- this.forceModels = forceModels;
- }
- /** {@inheritDoc} */
- @Override
- public void handleStep(final ODEStateInterpolator interpolator) {
- // Get the grid points to compute
- final double[] interpolationPoints =
- interpolationgrid.getGridPoints(interpolator.getPreviousState().getTime(),
- interpolator.getCurrentState().getTime());
- final SpacecraftState[] meanStates = new SpacecraftState[interpolationPoints.length];
- for (int i = 0; i < interpolationPoints.length; ++i) {
- // Build the mean state interpolated at grid point
- final double time = interpolationPoints[i];
- final ODEStateAndDerivative sd = interpolator.getInterpolatedState(time);
- meanStates[i] = mapper.mapArrayToState(time,
- sd.getPrimaryState(),
- sd.getPrimaryDerivative(),
- PropagationType.MEAN);
- }
- // Computate short periodic coefficients for this step
- for (DSSTForceModel forceModel : forceModels) {
- forceModel.updateShortPeriodTerms(forceModel.getParametersAllValues(), meanStates);
- }
- }
- }
- }