Propagation in STK Components usually stops after a span of time. For example, a NumericalPropagator propagates for a given duration. However, what if you want to stop based on some other criteria? Maybe you want to stop after your aircraft reaches a certain amount of fuel, or maybe stop propagating a satellite at a certain true anomaly so that you can perform a maneuver. Stopping conditions enable you to detect these kinds of events to stop your propagator.
The functionality described in this topic requires a license for the Segment Propagation Library.
Stopping Conditions will evaluate a value at every step of propagation. That value is treated as a function and sampled by a JulianDateFunctionExplorer. The propagator will continue to step until the function explorer detects extrema or threshold crossings. See the Exploring Functions topic for more information on the function explorer. When events are detected, the function explorer will then drive the propagator, forcing it to take steps to the exact event (within the specified tolerances). Additional checks can be done (such as a counter or additional constraints), either when the event is first detected, or when it is precisely found.
Let's say you want to stop when your satellite reaches a certain altitude with a two-body propagator. To do this, you start by configuring your TwoBodyStoppablePropagator. The following code sample demonstrates this:
EarthCentralBody earth = CentralBodiesFacet.getFromContext().getEarth(); TwoBodyStoppablePropagator stoppablePropagator = new TwoBodyStoppablePropagator(); // configure basic settings stoppablePropagator.setStep(Duration.fromSeconds(120.0)); stoppablePropagator.setPropagationFrame(earth.getInertialFrame()); stoppablePropagator.setGravitationalParameter(WorldGeodeticSystem1984.GravitationalParameter); stoppablePropagator.setPropagationPointIdentification("SatelliteMotion"); // add the altitude stopping condition ScalarCartographicElement altitudeScalar = new ScalarCartographicElement( earth, stoppablePropagator.getPropagationPoint(), CartographicElement.HEIGHT); ScalarStoppingCondition altitudeStoppingCondition = new ScalarStoppingCondition( altitudeScalar, 500000.0, // 500 km threshold (since this is a cartographic location though, it is measured in the fixed frame) 0.01, // 0.01 meter tolerance StopType.ANY_THRESHOLD); altitudeStoppingCondition.setName("Altitude_stopping_condition"); stoppablePropagator.getStoppingConditions().add(altitudeStoppingCondition); // it is good to add a maximum duration stopping condition just in case DurationStoppingCondition maximumDuration = new DurationStoppingCondition(Duration.fromDays(2.0)); maximumDuration.setName("Maximum_duration_stopping_condition"); stoppablePropagator.getStoppingConditions().add(maximumDuration); // get the propagator SinglePointStoppablePropagator propagator = stoppablePropagator.getSinglePointPropagator(); // create the initial conditions JulianDate initialDate = TimeConstants.J2000; Motion1<Cartesian> initialState = new Motion1<>(new Cartesian(6600000.0, 0.0, 0.0), new Cartesian(0.0, 5800.0, 5800.0)); // create propagator results SinglePointStoppablePropagatorResults results = propagator.propagateUntilStop(initialDate, initialState, IntegrationSense.INCREASING, null); Motion1<Cartesian> finalEphemerisPoint = results.getFinalState().getMotion(stoppablePropagator.getPropagationPointIdentification()); JulianDate finalDate = results.getFinalDate(); Cartographic finalCartographic = earth.getShape().cartesianToCartographic(convertFromInertialToFixed(finalDate, finalEphemerisPoint).getValue()); // the height will be 500 km
There is also a StoppableNumericalPropagator. Using this type is similar to the two-body example above, but take care to not lose track of the IntegrationPoint (get) you are using. The following code sample demonstrates this:
EarthCentralBody earth = CentralBodiesFacet.getFromContext().getEarth(); StoppableNumericalPropagator stoppablePropagator = new StoppableNumericalPropagator(); PropagationNewtonianPoint propagationPoint = new PropagationNewtonianPoint(); propagationPoint.setIdentification("SatelliteMotion"); // initial values must be set, but these will get reset with the passed in initial state. propagationPoint.setInitialPosition(new Cartesian(500.0, 0.0, 0.0)); propagationPoint.setInitialVelocity(new Cartesian(0.0, 100.0, 100.0)); propagationPoint.setMass(new ScalarFixed(500)); TwoBodyGravity twoBodyForceModel = new TwoBodyGravity(propagationPoint.getIntegrationPoint(), earth, WorldGeodeticSystem1984.GravitationalParameter); propagationPoint.getAppliedForces().add(twoBodyForceModel); RungeKutta4Integrator integrator = new RungeKutta4Integrator(); integrator.setInitialStepSize(30.0); NumericalPropagatorDefinition numericalPropagator = new NumericalPropagatorDefinition(); numericalPropagator.setEpoch(TimeConstants.J2000); numericalPropagator.getIntegrationElements().add(propagationPoint); numericalPropagator.setIntegrator(integrator); stoppablePropagator.setPropagatorDefinition(numericalPropagator); // add the altitude stopping condition ScalarCartographicElement altitudeScalar = new ScalarCartographicElement( earth, propagationPoint.getIntegrationPoint(), // the integration point must be used here. It will get updated with every evaluation of the numerical propagator CartographicElement.HEIGHT); ScalarStoppingCondition altitudeStoppingCondition = new ScalarStoppingCondition( altitudeScalar, 500000.0, // 500 km threshold (since this is a cartographic location though, it is measured in the fixed frame) 0.01, // 0.01 meter tolerance StopType.ANY_THRESHOLD); altitudeStoppingCondition.setName("Altitude_stopping_condition"); stoppablePropagator.getStoppingConditions().add(altitudeStoppingCondition); // it is good to add a maximum duration stopping condition just in case DurationStoppingCondition maximumDuration = new DurationStoppingCondition(Duration.fromDays(2.0)); maximumDuration.setName("Maximum_duration_stopping_condition"); stoppablePropagator.getStoppingConditions().add(maximumDuration); // get the propagator StoppablePropagator propagator = stoppablePropagator.getStoppablePropagator(); Motion1<Cartesian> initialMotion = new Motion1<>(new Cartesian(6600000.0, 0.0, 0.0), new Cartesian(0.0, 5800.0, 5800.0)); BasicState initialState = new BasicState(); initialState.addStateElementMotion(propagationPoint.getIdentification(), initialMotion); initialState.setCurrentDate(TimeConstants.J2000); // create propagator results StoppablePropagatorResults results = propagator.propagateUntilStop(initialState, null); Motion1<Cartesian> finalEphemerisPoint = results.getFinalState().getMotion(propagationPoint.getIdentification()); JulianDate finalDate = results.getFinalDate(); Cartographic finalCartographic = earth.getShape().cartesianToCartographic(convertFromInertialToFixed(finalDate, finalEphemerisPoint).getValue()); // the height will be 500 km
In both cases, you can confirm that the altitude of the satellite at the end of its propagation is indeed at the requested altitude.
Notice that the definition of the altitude scalar and stopping condition relied on a placeholder point in both examples. Because the altitude will need to be computed based on the propagated state at each step (so as to check if the threshold has been crossed), the placeholder point will need to be used. As a result, individual StoppingConditions are often specific to a particular StoppablePropagator instance.
StoppablePropagators are generally meant to propagate starting from a given ITimeBasedState. However, many of the actual StoppablePropagators can be configured with an optional initial state. If an initial state is passed in, the default initial state will be ignored, unless otherwise stated.
Since these propagators propagate from a passed in state, StateElementAdapters can - and in some cases must - be specified for the elements getting propagated. These adapters will perform the necessary ReferenceFrame transformations, or other transformations. If the passed in state is already in the correct frame, then adapters may not be needed. Adapters will also be ignored if the initial state that the propagator was configured with is used.
You are able to provide a list of stopping conditions to the propagator, but all it takes is one condition to be satisfied for propagation to stop. If you want to define other criteria for when to stop propagating, you can add StoppingConditionConstraints to your condition. Using the above example, if you wanted to stop propagating at a 500km altitude, but only after having propagated for 5 days, you could do it with two propagate segments, but it is simpler to add a DurationStoppingConditionConstraint to the altitude condition, as shown in the following code sample:
propagatorDefinition.getAuxiliaryElements().add(propagatedAltitude); ValueInStateStoppingCondition stoppingCondition = new ValueInStateStoppingCondition( propagatedAltitude.getIdentification(), 500000, // threshold meters 0.1, // tolerance, meters StopType.ANY_THRESHOLD); DurationStoppingConditionConstraint constraint = new DurationStoppingConditionConstraint( Duration.fromDays(5.0), // threshold Duration.fromSeconds(1.0), // tolerance WhenToCheckConstraint.WHEN_EVENT_IS_DETECTED, InequalityCondition.GREATER_THAN, false); // use the absolute value of the propagation duration stoppingCondition.getConstraints().add(constraint);
Constraints can be checked at one of two times: when a stop is detected or when the stop is exactly found. Checking the constraint when a stop is first detected can improve performance if finding the exact event of the stopping condition is costly. However, if the relevant value of your constraint may change significantly between when the event is detected and the time at its exact solution, checking it at the exact stop will ensure that the constraint is applied more precisely.
Note that StoppingConditions have a property that indicates whether to stop after an event has been found more than once. If you wanted to stop on the second time your satellite's altitude was 500 km, set StopOnEventNumber (get / set) to 2. When a StoppingConditionConstraint prevents a StoppingCondition from stopping propagation, that event that was detected will not be added to the count of events found.
There are some cases where StoppingConditions will be automatically added to a StoppablePropagator. If the availability of the underlying evaluator or propagator that is generating states is not infinite, a pair of EpochSecondsStoppingConditions can be added by calling addEndOfAvailabilityStoppingConditions, in order to prevent going outside of the availability.
There are several StoppablePropagators that are included with STK Components.
StoppableNumericalPropagator - This propagator takes a NumericalPropagator and the integrator will control the stepping until an stopping condition event is detected. If no initial state is passed to the propagateUntilStop method, then the initial state of the NumericalPropagator will be used. If geometry types are needed for the stopping conditions, configure them to use the IntegrationPoint (get) or similar elements on the PropagationStateElement.
SinglePointStoppablePropagator - This abstract base class requires derived types to provide a PointEvaluator, which will be used to create a state at the required times. This propagator has a PropagationPoint (get) that can be used to construct geometry items that can be used in its StoppingConditions.
ConstantStateStoppablePropagator - This propagator will take the initial state passed into it at propagation time, and return copies of the state with the time incremented. An initial state must be passed to it at propagation time. This can be useful for modeling a landing or docking.
DynamicStateStoppablePropagator - Propagates a DynamicState<T>. This can be useful when you want to propagate a set of geometry items, such as Points and Scalars. However, this stoppable propagator cannot be initialized from an initial state.
The provided PropagateSegments all use stopping conditions to end their propagation. PropagateSegment simply wraps and handles configuration and propagation of the wrapped StoppablePropagator (get / set). All adapters defined on the segment will get passed to the wrapped StoppablePropagator.
The StoppingConditions (get) are the same as the StoppingConditions (get) on the wrapped StoppablePropagatorDefinition. However, if the PropagateSegment is configured with a MaximumDuration (get / set), that will be added automatically to the wrapped stoppable propagator.
Each stopping condition in the PropagateSegment can execute an optional segment after it. By configuring an auto-segment by calling setStoppingConditionAutoSegment, you can treat the stopping conditions as an 'if' condition, propagating a different segment based on which condition was satisfied. The following code sample demonstrates this:
EarthCentralBody earth = CentralBodiesFacet.getFromContext().getEarth(); PropagateSegment geoOrbit = createAndConfigurePropagateSegment(); // we want to stay in our Geo Box, so add a stopping condition for when we violate our latitude limits Scalar verticalLimitStoppingConditionScalar = new ScalarAbsoluteValue(new ScalarCartographicElement(earth, parameterizedPoint, CartographicElement.LATITUDE)); ScalarStoppingCondition verticalStoppingCondition = new ScalarStoppingCondition(verticalLimitStoppingConditionScalar, 0.1, 0.001, StopType.ANY_THRESHOLD); geoOrbit.getStoppingConditions().add(verticalStoppingCondition); // and repeat this stopping condition 5 times geoOrbit.setStoppingConditionAutoSegment(verticalStoppingCondition, getVerticalCorrectionSegmentList(), 5); // in the same way, do the longitude violation stopping condition Scalar horizontalLimitStoppingCondition = new ScalarAbsoluteValue(Scalar.subtract(Scalar.toScalar(Trig.degreesToRadians(-140.0)), new ScalarCartographicElement(earth, parameterizedPoint, CartographicElement.LONGITUDE))); ScalarStoppingCondition horizontalStoppingCondition = new ScalarStoppingCondition(horizontalLimitStoppingCondition, 0.1, 0.001, StopType.ANY_THRESHOLD); geoOrbit.getStoppingConditions().add(horizontalStoppingCondition); // repeat the condition 5 times too geoOrbit.setStoppingConditionAutoSegment(horizontalStoppingCondition, getHorizontalCorrectionSegmentList(), 5); // propagate SegmentPropagator propagator = geoOrbit.getSegmentPropagator(); PropagateSegmentResults results = (PropagateSegmentResults) propagator.propagate(); // notice that results has many items in its SegmentResults
Unless you know for sure that a stopping condition will be satisfied, you should generally include a DurationStoppingCondition that is guaranteed to not continue on forever. Also note that the PropagateSegment has a MaximumDuration (get / set) property to ensure propagation will indeed stop. It can be turned off if desired.
Scalars with discontinuities can confuse the system. A common example is stopping based on some true anomaly. The true anomaly will have a discontinuity at one of the nodes and that would technically be a threshold crossing. One way to get around this would be to square the scalar and adjust your threshold accordingly:
double trueAnomalyIWantToStopAt = Math.PI / 4.0; ScalarModifiedKeplerianElement trueAnomalyScalar = new ScalarModifiedKeplerianElement( gravity, satellitesPoint, KeplerianElement.TRUE_ANOMALY, earth.getInternationalCelestialReferenceFrame()); ScalarExponent trueAnomalySquaredScalar = new ScalarExponent( trueAnomalyScalar, Scalar.toScalar(2.0)); AuxiliaryStateScalar propagatedTrueAnomalySquared = new AuxiliaryStateScalar(); propagatedTrueAnomalySquared.setIdentification("true anomaly squared"); propagatedTrueAnomalySquared.setAuxiliaryScalar(trueAnomalySquaredScalar); ValueInStateStoppingCondition trueAnomalySquaredStoppingCondition = new ValueInStateStoppingCondition( propagatedTrueAnomalySquared.getIdentification(), 0.0, // threshold, radians squared 0.01, // tolerance, radians squared StopType.ANY_THRESHOLD); trueAnomalySquaredStoppingCondition.setThreshold(ValueDefinition.toValueDefinition(Math.pow(trueAnomalyIWantToStopAt, 2.0)));
ScalarStoppingCondition periapsisStoppingCondition = new ScalarStoppingCondition( trueAnomalyScalar, 0.0, // threshold, radians 0.001, // value tolerance, radians StopType.ANY_THRESHOLD); periapsisStoppingCondition.setAngularSetting(CircularRange.NEGATIVE_PI_TO_PI);
When using a DurationStoppingCondition or a DurationStoppingConditionConstraint, the sign of the Duration matters. If you are propagating backwards, you must set your Duration to be negative as well, or else those conditions and constraints will never be satisfied.
NumericalPropagator has a propagateUntilStop method that takes a list of stopping conditions. Generally it is best to use a NumericalPropagatorSegment, but if you want to manage the actual stopping conditions and NumericalPropagator yourself, you can. In this example, the altitude stopping condition from the first example above is passed into the NumericalPropagator directly:
ArrayList<StoppingConditionEvaluator> conditions = new ArrayList<>(); EvaluatorGroup group = new EvaluatorGroup(); NumericalPropagator propagator = numericalPropagator.createPropagator(group); StoppingConditionEvaluator condition = stoppingCondition.getEvaluator(group); conditions.add(condition); StoppableNumericalPropagatorResults results = propagator.propagateUntilStop(conditions, null); DateMotionCollection1<Cartesian> ephemeris = results.getPropagationHistory().getDateMotionCollection(satelliteMotionIdentifier);