AbstractIntegratedPropagator.java

/* Copyright 2002-2021 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.integration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hipparchus.exception.MathRuntimeException;
import org.hipparchus.ode.DenseOutputModel;
import org.hipparchus.ode.EquationsMapper;
import org.hipparchus.ode.ExpandableODE;
import org.hipparchus.ode.ODEIntegrator;
import org.hipparchus.ode.ODEState;
import org.hipparchus.ode.ODEStateAndDerivative;
import org.hipparchus.ode.OrdinaryDifferentialEquation;
import org.hipparchus.ode.SecondaryODE;
import org.hipparchus.ode.events.Action;
import org.hipparchus.ode.events.EventHandlerConfiguration;
import org.hipparchus.ode.events.ODEEventHandler;
import org.hipparchus.ode.sampling.AbstractODEStateInterpolator;
import org.hipparchus.ode.sampling.ODEStateInterpolator;
import org.hipparchus.ode.sampling.ODEStepHandler;
import org.hipparchus.util.Precision;
import org.orekit.attitudes.AttitudeProvider;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.orbits.OrbitType;
import org.orekit.orbits.PositionAngle;
import org.orekit.propagation.AbstractPropagator;
import org.orekit.propagation.BoundedPropagator;
import org.orekit.propagation.EphemerisGenerator;
import org.orekit.propagation.PropagationType;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.events.EventDetector;
import org.orekit.propagation.sampling.OrekitStepHandler;
import org.orekit.propagation.sampling.OrekitStepInterpolator;
import org.orekit.time.AbsoluteDate;


/** Common handling of {@link org.orekit.propagation.Propagator Propagator}
 *  methods for both numerical and semi-analytical propagators.
 *  @author Luc Maisonobe
 */
public abstract class AbstractIntegratedPropagator extends AbstractPropagator {

    /** Event detectors not related to force models. */
    private final List<EventDetector> detectors;

    /** Step handlers dedicated to ephemeris generation. */
    private final List<StoringStepHandler> generators;

    /** Integrator selected by the user for the orbital extrapolation process. */
    private final ODEIntegrator integrator;

    /** Additional equations. */
    private List<AdditionalEquations> additionalEquations;

    /** Counter for differential equations calls. */
    private int calls;

    /** Mapper between raw double components and space flight dynamics objects. */
    private StateMapper stateMapper;

    /** Equations mapper. */
    private EquationsMapper equationsMapper;

    /** Flag for resetting the state at end of propagation. */
    private boolean resetAtEnd;

    /** Type of orbit to output (mean or osculating) <br/>
     * <p>
     * This is used only in the case of semianalitical propagators where there is a clear separation between
     * mean and short periodic elements. It is ignored by the Numerical propagator.
     * </p>
     */
    private PropagationType propagationType;

    /** Build a new instance.
     * @param integrator numerical integrator to use for propagation.
     * @param propagationType type of orbit to output (mean or osculating).
     */
    protected AbstractIntegratedPropagator(final ODEIntegrator integrator, final PropagationType propagationType) {
        detectors            = new ArrayList<>();
        generators           = new ArrayList<>();
        additionalEquations  = new ArrayList<>();
        this.integrator      = integrator;
        this.propagationType = propagationType;
        this.resetAtEnd      = true;
    }

    /** Allow/disallow resetting the initial state at end of propagation.
     * <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. Calling this method with {@code resetAtEnd} set
     * to false changes prevents such reset.
     * </p>
     * @param resetAtEnd if true, at end of each propagation, the {@link
     * #getInitialState() initial state} will be reset to the final state of
     * the propagation, otherwise the initial state will be preserved
     * @since 9.0
     */
    public void setResetAtEnd(final boolean resetAtEnd) {
        this.resetAtEnd = resetAtEnd;
    }

    /** Initialize the mapper. */
    protected void initMapper() {
        stateMapper = createMapper(null, Double.NaN, null, null, null, null);
    }

    /**  {@inheritDoc} */
    public void setAttitudeProvider(final AttitudeProvider attitudeProvider) {
        super.setAttitudeProvider(attitudeProvider);
        stateMapper = createMapper(stateMapper.getReferenceDate(), stateMapper.getMu(),
                                   stateMapper.getOrbitType(), stateMapper.getPositionAngleType(),
                                   attitudeProvider, stateMapper.getFrame());
    }

    /** Set propagation orbit type.
     * @param orbitType orbit type to use for propagation, null for
     * propagating using {@link org.orekit.utils.AbsolutePVCoordinates AbsolutePVCoordinates}
     * rather than {@link org.orekit.orbits Orbit}
     */
    protected void setOrbitType(final OrbitType orbitType) {
        stateMapper = createMapper(stateMapper.getReferenceDate(), stateMapper.getMu(),
                                   orbitType, stateMapper.getPositionAngleType(),
                                   stateMapper.getAttitudeProvider(), stateMapper.getFrame());
    }

    /** Get propagation parameter type.
     * @return orbit type used for propagation, null for
     * propagating using {@link org.orekit.utils.AbsolutePVCoordinates AbsolutePVCoordinates}
     * rather than {@link org.orekit.orbits Orbit}
     */
    protected OrbitType getOrbitType() {
        return stateMapper.getOrbitType();
    }

    /** Check if only the mean elements should be used in a semianalitical propagation.
     * @return {@link PropagationType MEAN} if only mean elements have to be used or
     *         {@link PropagationType OSCULATING} if osculating elements have to be also used.
     */
    protected PropagationType isMeanOrbit() {
        return propagationType;
    }

    /** Set position angle type.
     * <p>
     * The position parameter type is meaningful only if {@link
     * #getOrbitType() propagation orbit type}
     * support it. As an example, it is not meaningful for propagation
     * in {@link OrbitType#CARTESIAN Cartesian} parameters.
     * </p>
     * @param positionAngleType angle type to use for propagation
     */
    protected void setPositionAngleType(final PositionAngle positionAngleType) {
        stateMapper = createMapper(stateMapper.getReferenceDate(), stateMapper.getMu(),
                                   stateMapper.getOrbitType(), positionAngleType,
                                   stateMapper.getAttitudeProvider(), stateMapper.getFrame());
    }

    /** Get propagation parameter type.
     * @return angle type to use for propagation
     */
    protected PositionAngle getPositionAngleType() {
        return stateMapper.getPositionAngleType();
    }

    /** Set the central attraction coefficient μ.
     * @param mu central attraction coefficient (m³/s²)
     */
    public void setMu(final double mu) {
        stateMapper = createMapper(stateMapper.getReferenceDate(), mu,
                                   stateMapper.getOrbitType(), stateMapper.getPositionAngleType(),
                                   stateMapper.getAttitudeProvider(), stateMapper.getFrame());
    }

    /** Get the central attraction coefficient μ.
     * @return mu central attraction coefficient (m³/s²)
     * @see #setMu(double)
     */
    public double getMu() {
        return stateMapper.getMu();
    }

    /** Get the number of calls to the differential equations computation method.
     * <p>The number of calls is reset each time the {@link #propagate(AbsoluteDate)}
     * method is called.</p>
     * @return number of calls to the differential equations computation method
     */
    public int getCalls() {
        return calls;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isAdditionalStateManaged(final String name) {

        // first look at already integrated states
        if (super.isAdditionalStateManaged(name)) {
            return true;
        }

        // then look at states we integrate ourselves
        for (final AdditionalEquations equation : additionalEquations) {
            if (equation.getName().equals(name)) {
                return true;
            }
        }

        return false;
    }

    /** {@inheritDoc} */
    @Override
    public String[] getManagedAdditionalStates() {
        final String[] alreadyIntegrated = super.getManagedAdditionalStates();
        final String[] managed = new String[alreadyIntegrated.length + additionalEquations.size()];
        System.arraycopy(alreadyIntegrated, 0, managed, 0, alreadyIntegrated.length);
        for (int i = 0; i < additionalEquations.size(); ++i) {
            managed[i + alreadyIntegrated.length] = additionalEquations.get(i).getName();
        }
        return managed;
    }

    /** Add a set of user-specified equations to be integrated along with the orbit propagation.
     * @param additional additional equations
     */
    public void addAdditionalEquations(final AdditionalEquations additional) {

        // check if the name is already used
        if (isAdditionalStateManaged(additional.getName())) {
            // this set of equations is already registered, complain
            throw new OrekitException(OrekitMessages.ADDITIONAL_STATE_NAME_ALREADY_IN_USE,
                                      additional.getName());
        }

        // this is really a new set of equations, add it
        additionalEquations.add(additional);

    }

    /** {@inheritDoc} */
    public void addEventDetector(final EventDetector detector) {
        detectors.add(detector);
    }

    /** {@inheritDoc} */
    public Collection<EventDetector> getEventsDetectors() {
        return Collections.unmodifiableCollection(detectors);
    }

    /** {@inheritDoc} */
    public void clearEventsDetectors() {
        detectors.clear();
    }

    /** Set up all user defined event detectors.
     */
    protected void setUpUserEventDetectors() {
        for (final EventDetector detector : detectors) {
            setUpEventDetector(integrator, detector);
        }
    }

    /** Wrap an Orekit event detector and register it to the integrator.
     * @param integ integrator into which event detector should be registered
     * @param detector event detector to wrap
     */
    protected void setUpEventDetector(final ODEIntegrator integ, final EventDetector detector) {
        integ.addEventHandler(new AdaptedEventDetector(detector),
                              detector.getMaxCheckInterval(),
                              detector.getThreshold(),
                              detector.getMaxIterationCount());
    }

    /** {@inheritDoc} */
    @Override
    public EphemerisGenerator getEphemerisGenerator() {
        final StoringStepHandler storingHandler = new StoringStepHandler();
        generators.add(storingHandler);
        return storingHandler;
    }

    /** Create a mapper between raw double components and spacecraft state.
    /** Simple constructor.
     * <p>
     * The position parameter type is meaningful only if {@link
     * #getOrbitType() propagation orbit type}
     * support it. As an example, it is not meaningful for propagation
     * in {@link OrbitType#CARTESIAN Cartesian} parameters.
     * </p>
     * @param referenceDate reference date
     * @param mu central attraction coefficient (m³/s²)
     * @param orbitType orbit type to use for mapping
     * @param positionAngleType angle type to use for propagation
     * @param attitudeProvider attitude provider
     * @param frame inertial frame
     * @return new mapper
     */
    protected abstract StateMapper createMapper(AbsoluteDate referenceDate, double mu,
                                                OrbitType orbitType, PositionAngle positionAngleType,
                                                AttitudeProvider attitudeProvider, Frame frame);

    /** Get the differential equations to integrate (for main state only).
     * @param integ numerical integrator to use for propagation.
     * @return differential equations for main state
     */
    protected abstract MainStateEquations getMainStateEquations(ODEIntegrator integ);

    /** {@inheritDoc} */
    public SpacecraftState propagate(final AbsoluteDate target) {
        if (getStartDate() == null) {
            if (getInitialState() == null) {
                throw new OrekitException(OrekitMessages.INITIAL_STATE_NOT_SPECIFIED_FOR_ORBIT_PROPAGATION);
            }
            setStartDate(getInitialState().getDate());
        }
        return propagate(getStartDate(), target);
    }

    /** {@inheritDoc} */
    public SpacecraftState propagate(final AbsoluteDate tStart, final AbsoluteDate tEnd) {

        if (getInitialState() == null) {
            throw new OrekitException(OrekitMessages.INITIAL_STATE_NOT_SPECIFIED_FOR_ORBIT_PROPAGATION);
        }

        // make sure the integrator will be reset properly even if we change its events handlers and step handlers
        try (IntegratorResetter resetter = new IntegratorResetter(integrator)) {

            if (!tStart.equals(getInitialState().getDate())) {
                // if propagation start date is not initial date,
                // propagate from initial to start date without event detection
                integrateDynamics(tStart);
            }

            // set up events added by user
            setUpUserEventDetectors();

            // set up step handlers
            for (final OrekitStepHandler handler : getMultiplexer().getHandlers()) {
                integrator.addStepHandler(new AdaptedStepHandler(handler));
            }
            for (final StoringStepHandler generator : generators) {
                generator.setEndDate(tEnd);
                integrator.addStepHandler(generator);
            }

            // propagate from start date to end date with event detection
            return integrateDynamics(tEnd);

        }

    }

    /** Propagation with or without event detection.
     * @param tEnd target date to which orbit should be propagated
     * @return state at end of propagation
     */
    private SpacecraftState integrateDynamics(final AbsoluteDate tEnd) {
        try {

            initializePropagation();

            if (getInitialState().getDate().equals(tEnd)) {
                // don't extrapolate
                return getInitialState();
            }

            // space dynamics view
            stateMapper = createMapper(getInitialState().getDate(), stateMapper.getMu(),
                                       stateMapper.getOrbitType(), stateMapper.getPositionAngleType(),
                                       stateMapper.getAttitudeProvider(), getInitialState().getFrame());


            if (Double.isNaN(getMu())) {
                setMu(getInitialState().getMu());
            }

            if (getInitialState().getMass() <= 0.0) {
                throw new OrekitException(OrekitMessages.SPACECRAFT_MASS_BECOMES_NEGATIVE,
                                          getInitialState().getMass());
            }

            // convert space flight dynamics API to math API
            final SpacecraftState initialIntegrationState = getInitialIntegrationState();
            final ODEState mathInitialState = createInitialState(initialIntegrationState);
            final ExpandableODE mathODE = createODE(integrator, mathInitialState);
            equationsMapper = mathODE.getMapper();

            // mathematical integration
            final ODEStateAndDerivative mathFinalState;
            beforeIntegration(initialIntegrationState, tEnd);
            mathFinalState = integrator.integrate(mathODE, mathInitialState,
                                                  tEnd.durationFrom(getInitialState().getDate()));
            afterIntegration();

            // get final state
            SpacecraftState finalState =
                            stateMapper.mapArrayToState(stateMapper.mapDoubleToDate(mathFinalState.getTime(),
                                                                                    tEnd),
                                                        mathFinalState.getPrimaryState(),
                                                        mathFinalState.getPrimaryDerivative(),
                                                        propagationType);

            finalState = updateAdditionalStates(finalState);
            for (int i = 0; i < additionalEquations.size(); ++i) {
                final double[] secondary = mathFinalState.getSecondaryState(i + 1);
                finalState = finalState.addAdditionalState(additionalEquations.get(i).getName(),
                                                           secondary);
            }
            if (resetAtEnd) {
                resetInitialState(finalState);
                setStartDate(finalState.getDate());
            }

            return finalState;

        } catch (MathRuntimeException mre) {
            throw OrekitException.unwrap(mre);
        }
    }

    /** Get the initial state for integration.
     * @return initial state for integration
     */
    protected SpacecraftState getInitialIntegrationState() {
        return getInitialState();
    }

    /** Create an initial state.
     * @param initialState initial state in flight dynamics world
     * @return initial state in mathematics world
     */
    private ODEState createInitialState(final SpacecraftState initialState) {

        // retrieve initial state
        final double[] primary  = new double[getBasicDimension()];
        stateMapper.mapStateToArray(initialState, primary, null);

        // secondary part of the ODE
        final double[][] secondary = new double[additionalEquations.size()][];
        for (int i = 0; i < additionalEquations.size(); ++i) {
            final AdditionalEquations additional = additionalEquations.get(i);
            secondary[i] = initialState.getAdditionalState(additional.getName());
        }

        return new ODEState(0.0, primary, secondary);

    }

    /** Create an ODE with all equations.
     * @param integ numerical integrator to use for propagation.
     * @param mathInitialState initial state
     * @return a new ode
     */
    private ExpandableODE createODE(final ODEIntegrator integ,
                                    final ODEState mathInitialState) {

        final ExpandableODE ode =
                new ExpandableODE(new ConvertedMainStateEquations(getMainStateEquations(integ)));

        // secondary part of the ODE
        for (int i = 0; i < additionalEquations.size(); ++i) {
            final AdditionalEquations additional = additionalEquations.get(i);
            final SecondaryODE secondary =
                    new ConvertedSecondaryStateEquations(additional,
                                                         mathInitialState.getSecondaryStateDimension(i + 1));
            ode.addSecondaryEquations(secondary);
        }

        return ode;

    }

    /** 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
     */
    protected void beforeIntegration(final SpacecraftState initialState,
                                     final AbsoluteDate tEnd) {
        // do nothing by default
    }

    /** Method called just after integration.
     * <p>
     * The default implementation does nothing, it may be specialized in subclasses.
     * </p>
     */
    protected void afterIntegration() {
        // do nothing by default
    }

    /** Get state vector dimension without additional parameters.
     * @return state vector dimension without additional parameters.
     */
    public int getBasicDimension() {
        return 7;

    }

    /** Get the integrator used by the propagator.
     * @return the integrator.
     */
    protected ODEIntegrator getIntegrator() {
        return integrator;
    }

    /** Get a complete state with all additional equations.
     * @param t current value of the independent <I>time</I> variable
     * @param y array containing the current value of the state vector
     * @param yDot array containing the current value of the state vector derivative
     * @return complete state
     */
    private SpacecraftState getCompleteState(final double t, final double[] y, final double[] yDot) {

        // main state
        SpacecraftState state = stateMapper.mapArrayToState(t, y, yDot, propagationType);

        // pre-integrated additional states
        state = updateAdditionalStates(state);

        // additional states integrated here
        if (!additionalEquations.isEmpty()) {

            for (int i = 0; i < additionalEquations.size(); ++i) {
                state = state.addAdditionalState(additionalEquations.get(i).getName(),
                                                 equationsMapper.extractEquationData(i + 1, y));
            }

        }

        return state;

    }

    /** Convert a state from mathematical world to space flight dynamics world.
     * @param os mathematical state
     * @return space flight dynamics state
     */
    private SpacecraftState convert(final ODEStateAndDerivative os) {

        SpacecraftState s =
                        stateMapper.mapArrayToState(os.getTime(),
                                                    os.getPrimaryState(),
                                                    os.getPrimaryDerivative(),
                                                    propagationType);
        s = updateAdditionalStates(s);
        for (int i = 0; i < additionalEquations.size(); ++i) {
            final double[] secondary = os.getSecondaryState(i + 1);
            s = s.addAdditionalState(additionalEquations.get(i).getName(), secondary);
        }

        return s;

    }

    /** Convert a state from space flight dynamics world to mathematical world.
     * @param state space flight dynamics state
     * @return mathematical state
     */
    private ODEStateAndDerivative convert(final SpacecraftState state) {

        // retrieve initial state
        final double[] primary    = new double[getBasicDimension()];
        final double[] primaryDot = new double[getBasicDimension()];
        stateMapper.mapStateToArray(state, primary, primaryDot);

        // secondary part of the ODE
        final double[][] secondary    = new double[additionalEquations.size()][];
        for (int i = 0; i < additionalEquations.size(); ++i) {
            final AdditionalEquations additional = additionalEquations.get(i);
            secondary[i] = state.getAdditionalState(additional.getName());
        }

        return new ODEStateAndDerivative(stateMapper.mapDateToDouble(state.getDate()),
                                         primary, primaryDot,
                                         secondary, null);

    }

    /** Differential equations for the main state (orbit, attitude and mass). */
    public interface MainStateEquations {

        /**
         * Initialize the equations at the start of propagation. This method will be
         * called before any calls to {@link #computeDerivatives(SpacecraftState)}.
         *
         * <p> The default implementation of this method does nothing.
         *
         * @param initialState initial state information at the start of propagation.
         * @param target       date of propagation. Not equal to {@code
         *                     initialState.getDate()}.
         */
        default void init(final SpacecraftState initialState, final AbsoluteDate target) {
        }

        /** Compute differential equations for main state.
         * @param state current state
         * @return derivatives of main state
         */
        double[] computeDerivatives(SpacecraftState state);

    }

    /** Differential equations for the main state (orbit, attitude and mass), with converted API. */
    private class ConvertedMainStateEquations implements OrdinaryDifferentialEquation {

        /** Main state equations. */
        private final MainStateEquations main;

        /** Simple constructor.
         * @param main main state equations
         */
        ConvertedMainStateEquations(final MainStateEquations main) {
            this.main = main;
            calls = 0;
        }

        /** {@inheritDoc} */
        public int getDimension() {
            return getBasicDimension();
        }

        @Override
        public void init(final double t0, final double[] y0, final double finalTime) {
            // update space dynamics view
            SpacecraftState initialState = stateMapper.mapArrayToState(t0, y0, null, PropagationType.MEAN);
            initialState = updateAdditionalStates(initialState);
            final AbsoluteDate target = stateMapper.mapDoubleToDate(finalTime);
            main.init(initialState, target);
        }

        /** {@inheritDoc} */
        public double[] computeDerivatives(final double t, final double[] y) {

            // increment calls counter
            ++calls;

            // update space dynamics view
            SpacecraftState currentState = stateMapper.mapArrayToState(t, y, null, PropagationType.MEAN);
            currentState = updateAdditionalStates(currentState);

            // compute main state differentials
            return main.computeDerivatives(currentState);

        }

    }

    /** Differential equations for the secondary state (Jacobians, user variables ...), with converted API. */
    private class ConvertedSecondaryStateEquations implements SecondaryODE {

        /** Additional equations. */
        private final AdditionalEquations equations;

        /** Dimension of the additional state. */
        private final int dimension;

        /** Simple constructor.
         * @param equations additional equations
         * @param dimension dimension of the additional state
         */
        ConvertedSecondaryStateEquations(final AdditionalEquations equations,
                                         final int dimension) {
            this.equations = equations;
            this.dimension = dimension;
        }

        /** {@inheritDoc} */
        @Override
        public int getDimension() {
            return dimension;
        }

        /** {@inheritDoc} */
        @Override
        public void init(final double t0, final double[] primary0,
                         final double[] secondary0, final double finalTime) {
            // update space dynamics view
            SpacecraftState initialState = stateMapper.mapArrayToState(t0, primary0, null, PropagationType.MEAN);
            initialState = updateAdditionalStates(initialState);
            initialState = initialState.addAdditionalState(equations.getName(), secondary0);
            final AbsoluteDate target = stateMapper.mapDoubleToDate(finalTime);
            equations.init(initialState, target);

        }

        /** {@inheritDoc} */
        @Override
        public double[] computeDerivatives(final double t, final double[] primary,
                                           final double[] primaryDot, final double[] secondary) {

            // update space dynamics view
            SpacecraftState currentState = stateMapper.mapArrayToState(t, primary, primaryDot, PropagationType.MEAN);
            currentState = updateAdditionalStates(currentState);
            currentState = currentState.addAdditionalState(equations.getName(), secondary);

            // compute additional derivatives
            final double[] secondaryDot = new double[secondary.length];
            final double[] additionalMainDot =
                            equations.computeDerivatives(currentState, secondaryDot);
            if (additionalMainDot != null) {
                // the additional equations have an effect on main equations
                for (int i = 0; i < additionalMainDot.length; ++i) {
                    primaryDot[i] += additionalMainDot[i];
                }
            }

            return secondaryDot;

        }

    }

    /** Adapt an {@link org.orekit.propagation.events.EventDetector}
     * to Hipparchus {@link org.hipparchus.ode.events.ODEEventHandler} interface.
     * @author Fabien Maussion
     */
    private class AdaptedEventDetector implements ODEEventHandler {

        /** Underlying event detector. */
        private final EventDetector detector;

        /** Time of the previous call to g. */
        private double lastT;

        /** Value from the previous call to g. */
        private double lastG;

        /** Build a wrapped event detector.
         * @param detector event detector to wrap
        */
        AdaptedEventDetector(final EventDetector detector) {
            this.detector = detector;
            this.lastT    = Double.NaN;
            this.lastG    = Double.NaN;
        }

        /** {@inheritDoc} */
        public void init(final ODEStateAndDerivative s0, final double t) {
            detector.init(getCompleteState(s0.getTime(), s0.getCompleteState(), s0.getCompleteDerivative()),
                          stateMapper.mapDoubleToDate(t));
            this.lastT = Double.NaN;
            this.lastG = Double.NaN;
        }

        /** {@inheritDoc} */
        public double g(final ODEStateAndDerivative s) {
            if (!Precision.equals(lastT, s.getTime(), 0)) {
                lastT = s.getTime();
                lastG = detector.g(getCompleteState(s.getTime(), s.getCompleteState(), s.getCompleteDerivative()));
            }
            return lastG;
        }

        /** {@inheritDoc} */
        public Action eventOccurred(final ODEStateAndDerivative s, final boolean increasing) {
            return detector.eventOccurred(
                    getCompleteState(
                            s.getTime(),
                            s.getCompleteState(),
                            s.getCompleteDerivative()),
                    increasing);
        }

        /** {@inheritDoc} */
        public ODEState resetState(final ODEStateAndDerivative s) {

            final SpacecraftState oldState = getCompleteState(s.getTime(), s.getCompleteState(), s.getCompleteDerivative());
            final SpacecraftState newState = detector.resetState(oldState);
            stateChanged(newState);

            // main part
            final double[] primary    = new double[s.getPrimaryStateDimension()];
            stateMapper.mapStateToArray(newState, primary, null);

            // secondary part
            final double[][] secondary    = new double[additionalEquations.size()][];
            for (int i = 0; i < additionalEquations.size(); ++i) {
                secondary[i] = newState.getAdditionalState(additionalEquations.get(i).getName());
            }

            return new ODEState(newState.getDate().durationFrom(getStartDate()),
                                primary, secondary);

        }

    }

    /** Adapt an {@link org.orekit.propagation.sampling.OrekitStepHandler}
     * to Hipparchus {@link ODEStepHandler} interface.
     * @author Luc Maisonobe
     */
    private class AdaptedStepHandler implements ODEStepHandler {

        /** Underlying handler. */
        private final OrekitStepHandler handler;

        /** Build an instance.
         * @param handler underlying handler to wrap
         */
        AdaptedStepHandler(final OrekitStepHandler handler) {
            this.handler = handler;
        }

        /** {@inheritDoc} */
        public void init(final ODEStateAndDerivative s0, final double t) {
            handler.init(getCompleteState(s0.getTime(), s0.getCompleteState(), s0.getCompleteDerivative()),
                         stateMapper.mapDoubleToDate(t));
        }

        /** {@inheritDoc} */
        @Override
        public void handleStep(final ODEStateInterpolator interpolator) {
            handler.handleStep(new AdaptedStepInterpolator(interpolator));
        }

        /** {@inheritDoc} */
        @Override
        public void finish(final ODEStateAndDerivative finalState) {
            handler.finish(convert(finalState));
        }

    }

    /** Adapt an Hipparchus {@link ODEStateInterpolator}
     * to an orekit {@link OrekitStepInterpolator} interface.
     * @author Luc Maisonobe
     */
    private class AdaptedStepInterpolator implements OrekitStepInterpolator {

        /** Underlying raw rawInterpolator. */
        private final ODEStateInterpolator mathInterpolator;

        /** Simple constructor.
         * @param mathInterpolator underlying raw interpolator
         */
        AdaptedStepInterpolator(final ODEStateInterpolator mathInterpolator) {
            this.mathInterpolator = mathInterpolator;
        }

        /** {@inheritDoc}} */
        @Override
        public SpacecraftState getPreviousState() {
            return convert(mathInterpolator.getPreviousState());
        }

        /** {@inheritDoc}} */
        @Override
        public boolean isPreviousStateInterpolated() {
            return mathInterpolator.isPreviousStateInterpolated();
        }

        /** {@inheritDoc}} */
        @Override
        public SpacecraftState getCurrentState() {
            return convert(mathInterpolator.getCurrentState());
        }

        /** {@inheritDoc}} */
        @Override
        public boolean isCurrentStateInterpolated() {
            return mathInterpolator.isCurrentStateInterpolated();
        }

        /** {@inheritDoc}} */
        @Override
        public SpacecraftState getInterpolatedState(final AbsoluteDate date) {
            return convert(mathInterpolator.getInterpolatedState(date.durationFrom(stateMapper.getReferenceDate())));
        }

        /** {@inheritDoc}} */
        @Override
        public boolean isForward() {
            return mathInterpolator.isForward();
        }

        /** {@inheritDoc}} */
        @Override
        public AdaptedStepInterpolator restrictStep(final SpacecraftState newPreviousState,
                                                    final SpacecraftState newCurrentState) {
            try {
                final AbstractODEStateInterpolator aosi = (AbstractODEStateInterpolator) mathInterpolator;
                return new AdaptedStepInterpolator(aosi.restrictStep(convert(newPreviousState),
                                                                     convert(newCurrentState)));
            } catch (ClassCastException cce) {
                // this should never happen
                throw new OrekitInternalError(cce);
            }
        }

    }

    /** Specialized step handler storing interpolators for ephemeris generation.
     * @since 11.0
     */
    private class StoringStepHandler implements ODEStepHandler, EphemerisGenerator {

        /** Underlying raw mathematical model. */
        private DenseOutputModel model;

        /** the user supplied end date. Propagation may not end on this date. */
        private AbsoluteDate endDate;

        /** Generated ephemeris. */
        private BoundedPropagator ephemeris;

        /** Set the end date.
         * @param endDate end date
         */
        public void setEndDate(final AbsoluteDate endDate) {
            this.endDate = endDate;
        }

        /** {@inheritDoc} */
        @Override
        public void init(final ODEStateAndDerivative s0, final double t) {

            this.model = new DenseOutputModel();
            model.init(s0, t);

            // ephemeris will be generated when last step is processed
            this.ephemeris = null;

        }

        /** {@inheritDoc} */
        @Override
        public BoundedPropagator getGeneratedEphemeris() {
            return ephemeris;
        }

        /** {@inheritDoc} */
        @Override
        public void handleStep(final ODEStateInterpolator interpolator) {
            model.handleStep(interpolator);
        }

        /** {@inheritDoc} */
        @Override
        public void finish(final ODEStateAndDerivative finalState) {

            // set up the boundary dates
            final double tI = model.getInitialTime();
            final double tF = model.getFinalTime();
            // tI is almost? always zero
            final AbsoluteDate startDate =
                            stateMapper.mapDoubleToDate(tI);
            final AbsoluteDate finalDate =
                            stateMapper.mapDoubleToDate(tF, this.endDate);
            final AbsoluteDate minDate;
            final AbsoluteDate maxDate;
            if (tF < tI) {
                minDate = finalDate;
                maxDate = startDate;
            } else {
                minDate = startDate;
                maxDate = finalDate;
            }

            // get the initial additional states that are not managed
            final Map<String, double[]> unmanaged = new HashMap<String, double[]>();
            for (final Map.Entry<String, double[]> initial : getInitialState().getAdditionalStates().entrySet()) {
                if (!isAdditionalStateManaged(initial.getKey())) {
                    // this additional state was in the initial state, but is unknown to the propagator
                    // we simply copy its initial value as is
                    unmanaged.put(initial.getKey(), initial.getValue());
                }
            }

            // get the names of additional states managed by differential equations
            final String[] names = new String[additionalEquations.size()];
            for (int i = 0; i < names.length; ++i) {
                names[i] = additionalEquations.get(i).getName();
            }

            // create the ephemeris
            ephemeris = new IntegratedEphemeris(startDate, minDate, maxDate,
                                                stateMapper, propagationType, model, unmanaged,
                                                getAdditionalStateProviders(), names);

        }

    }

    /** Wrapper for resetting an integrator handlers.
     * <p>
     * This class is intended to be used in a try-with-resource statement.
     * If propagator-specific event handlers and step handlers are added to
     * the integrator in the try block, they will be removed automatically
     * when leaving the block, so the integrator only keep its own handlers
     * between calls to {@link AbstractIntegratedPropagator#propagate(AbsoluteDate, AbsoluteDate).
     * </p>
     * @since 11.0
     */
    private static class IntegratorResetter implements AutoCloseable {

        /** Wrapped integrator. */
        private final ODEIntegrator integrator;

        /** Initial event handlers list. */
        private final List<EventHandlerConfiguration> eventHandlersConfigurations;

        /** Initial step handlers list. */
        private final List<ODEStepHandler> stepHandlers;

        /** Simple constructor.
         * @param integrator wrapped integrator
         */
        IntegratorResetter(final ODEIntegrator integrator) {
            this.integrator                  = integrator;
            this.eventHandlersConfigurations = new ArrayList<>(integrator.getEventHandlersConfigurations());
            this.stepHandlers                = new ArrayList<>(integrator.getStepHandlers());
        }

        /** {@inheritDoc}
         * <p>
         * Reset event handlers and step handlers back to the initial list
         * </p>
         */
        @Override
        public void close() {

            // reset event handlers
            integrator.clearEventHandlers();
            eventHandlersConfigurations.forEach(c -> integrator.addEventHandler(c.getEventHandler(),
                                                                                c.getMaxCheckInterval(),
                                                                                c.getConvergence(),
                                                                                c.getMaxIterationCount(),
                                                                                c.getSolver()));

            // reset step handlers
            integrator.clearStepHandlers();
            stepHandlers.forEach(stepHandler -> integrator.addStepHandler(stepHandler));

        }

    }

}