Relative Proximity Operations (RPO)
This tutorial explains the different trajectories typical of relative proximity operations feasible using the methods given in the relative sub-package of the control package.
The four trajectories presented here are:
- the forced circular motion,
- the natural circumnavigation motion,
- the teardrop phase,
- injection into a co-elliptic orbit.
In relative proximity operations, a spacecraft (called chaser) is performing maneuvers and relatively orbiting around another spacecraft called target.
The motion of the chaser can be either embedded in the propagation of the target using a RelativeProvider or propagated by an independent propagator.
The two methods will be presented hereafter.
First thing is to define the orbit of the target and the relative initial state of the chaser in the Local Orbital Frame of the target:
final double n = 0.0011569; // Mean motion of target's orbit
final double rTarget = FastMath.pow(Constants.EIGEN5C_EARTH_MU / (n * n), 1. / 3.); // Target orbit's radius
// Target's orbit
final KeplerianOrbit targetOrbit = new KeplerianOrbit(rTarget, 0.0, 0.0,
0.0, 0.0, 0.0,
PositionAngleType.MEAN, PositionAngleType.MEAN,
FramesFactory.getGCRF(), EPOCH, Constants.EIGEN5C_EARTH_MU);
// Start condition of the transfer, expressed in the target's LOF
final TimeStampedPVCoordinates pvtChaserInitial = new TimeStampedPVCoordinates(EPOCH, new Vector3D(1e3, -1.0e3, 0),
new Vector3D(-0.02e3, 0.02e3, -0.005e3));
Then a model to compute the relative operations must be chosen. Two models can be chosen:
-
Clohessy-Wiltshire model (CW)
- Reliable only for a target orbit in a circular orbit
- Using the QSW LOF of the target
- X axis aligned with position (Q) → Rbar
- Z axis aligned with orbital momentum (W) → Out of Plane (OOP) direction
- Y axis aligned with velocity (S), since the orbit is circular → Vbar
-
Yamanaka-Ankersen model (YA)
- Extends CW model to eccentric orbits
- Using the LVLH_CCSDS LOF of the target
- Z axis aligned with opposite of position → -Rbar
- Y axis aligned with opposite of orbital momentum → -OOP direction
- X axis completes the right-handed frame (aligned with velocity for circular orbits) → Vbar
// Define RPOModel to use.
final RPOModel rpoModel = RPOModel.YA;
To propagate the chaser motion using its own propagator in inertial frame, its initial state must be converted to the initial frame:
// Initial condition of the chaser expressed in the inertial frame.
final Transform lofToInertial = rpoModel.getLOFType().transformFromInertial(pvtChaserInitial.getDate(), targetOrbit.getPVCoordinates()).getInverse();
final PVCoordinates pvChaserInertial = lofToInertial.transformPVCoordinates(pvtChaserInitial);
final TimeStampedPVCoordinates pvtChaserInertial = new TimeStampedPVCoordinates(EPOCH, pvChaserInertial);
final KeplerianOrbit chaserOrbit = new KeplerianOrbit(pvtChaserInertial, targetOrbit.getFrame(), Constants.EIGEN5C_EARTH_MU);
We can now define the propagator of the target together with the chaser state provider and the independent propagator of the chaser:
// Define the chaser relative provider. Here YamanakaAnkersen provider to match with defined RPOModel.
final YamanakaAnkersenProvider yaProvider = new YamanakaAnkersenProvider(targetOrbit, pvtChaserInitial);
// Definition of the targetPropagator.
final KeplerianPropagator targetPropagator = new KeplerianPropagator(targetOrbit);
targetPropagator.addAdditionalDataProvider(yaProvider);
// Definition of the chaser propagator.
final KeplerianPropagator chaserPropagator = new KeplerianPropagator(chaserOrbit);
To ensure there is no date mismatch at start of the maneuvers we can propagate by only one second the chaser propagator to define date and position shared by the two propagators at start of the maneuvers:
// Propagate the target and the chaser provider by one second to handle first maneuver. The linear transfer
// will start at epoch.shiftedBy(1). PVT of the chaser at this particular time is chaserStartManeuver.
final SpacecraftState targetStart = targetPropagator.propagate(EPOCH.shiftedBy(1));
final double[] chaserStart = yaProvider.getAdditionalData(targetStart);
final TimeStampedPVCoordinates chaserStartManeuver = new TimeStampedPVCoordinates(EPOCH.shiftedBy(1),
new Vector3D(chaserStart[0], chaserStart[1], chaserStart[2]),
new Vector3D(chaserStart[3], chaserStart[4], chaserStart[5]));
// Reset target propagator, target orbit and initial state of the provider
targetPropagator.resetInitialState(new SpacecraftState(targetOrbit));
yaProvider.setInitialChaserPVTLof(pvtChaserInitial);
yaProvider.setTargetOrbit(targetOrbit);
The implementation of the propagation of these different trajectories will now diverge.
Forced circular motion
There is no natural orbit in which the chaser will be orbiting circularly around the target except under precise conditions. The forced circular motion allows the chaser to artificially orbit circularly around the target by performing a defined number of maneuvers.
From its initial position, the chaser follows a forced linear trajectory composed of multiple hops maneuvers before being inserted in the forced circular orbit. The maneuvers are performed at intermediary waypoints defined by the user. The waypoints are points in time and space.
First the circular waypoints are computed to define the motion on the forced circular orbit. It is possible to define:
- start date of the relative orbit,
- center offset of the orbit relative to target position,
- radius of the circular orbit,
- relative inclination of the orbit,
- relative raan of the orbit,
- period of the relative orbit,
- the number of waypoints (i.e. number of maneuvers) to perform the motion,
- number of revolutions,
- start angle (0° = along -VBar; 90° = along RBar)
- boolean to turn around the target clockwise or counter-clockwise.
// Compute the circular waypoints.
final List<TimeStampedPVCoordinates> circularWaypoints = rpoModel.computeForcedCircularMotionWaypoints(
CIRCULAR_START_DATE, CENTER_OFFSET, RADIUS, INCLINATION, RAAN, ORBIT_DURATION, ORBIT_POINTS,
NUMBER_OF_REVOLUTIONS, START_ANGLE, RETROGRADE);
Then it is possible to compute the linear transfer from the initial position of the chaser to the first waypoint of the relative forced circular orbit.
// Definition of the linear path. The linear path starts at chaserStartManeuver (NOT pvtChaserInitial) and ends
// at injection point of the relative orbit which is the first waypoint of the circularWaypoints list.
final List<TimeStampedPVCoordinates> waypoints = rpoModel.computeForcedLinearWaypoints(chaserStartManeuver,
circularWaypoints.get(0), 10);
By doing so, the last waypoint of the linear transfer and the first waypoint of the circular orbit are the same, so one of them must be removed before concatenating the two list of waypoints and computing the relative maneuvers from them:
// Remove first waypoint of the circular trajectory to not count it twice.
circularWaypoints.remove(0);
// Add all the circular waypoints to the linear waypoints.
waypoints.addAll(circularWaypoints);
// Compute the Relative maneuvers and add to it to the target propagator.
final List<RelativeManeuver> maneuvers = rpoModel.computeForcedManeuvers(waypoints, chaserStartManeuver.getVelocity(), targetOrbit, yaProvider);
for (RelativeManeuver maneuver: maneuvers) {
targetPropagator.addEventDetector(maneuver);
}
// Convert the relative maneuvers to impulse maneuvers and add them to the chaser propagator.
final List<ImpulseManeuver> impulseManeuvers = rpoModel.convertToImpulseManeuver(maneuvers, targetOrbit, 0);
for (ImpulseManeuver impulseManeuver: impulseManeuvers) {
chaserPropagator.addEventDetector(impulseManeuver);
}
Finally, we can propagate the two spacecrafts.
// Propagate the target and the chaser.
targetPropagator.propagate(FINAL_DATE);
chaserPropagator.propagate(FINAL_DATE);
In the figure, Trajectory A is computed directly from the relative provider and Trajectory B is computed by the independent chaser propagator.

The complete code for this example can be found in the source tree of the tutorials, in file src/main/java/org/orekit/tutorials/control/relative/ForcedCircularPropagation.java.
Natural Circumnavigation (Circular target's orbit)
If the target orbit is in a circular orbit, it is possible to inject the chaser in a natural orbit in which the chaser spacecraft will naturally circumnavigate around the target orbit. Once injected in this orbit, the chaser will remain in this relative orbit around the target.
For that trajectory, target's orbit MUST be circular.
From its initial position, the chaser follows a forced linear trajectory composed of multiple hops maneuvers before being inserted in this orbit. The maneuvers are performed at intermediary waypoints defined by the user. The waypoints are points in time and space.
The natural relative orbit is only defined by its relative inclination and the relative distance between the origin of the local orbital frame (target) and the intersection point between Vbar axis and the relative orbit.
We first compute the waypoint at injection date in the relative orbit.
// Define the injection waypoint into the natural circumnavigation orbit.
final TimeStampedPVCoordinates injectionWaypoint = rpoModel.computeNaturalCircumnavigationInjectionCircular(
INJECTION_DATE,
DISTANCE_ALONG_VBAR,
INCLINATION,
targetOrbit.getKeplerianMeanMotion()
);
Then, we can compute the linear waypoints and the associated maneuvers to transfer the chaser from its initial state to the injection waypoint.
// Compute chaser linear waypoints to go linearly from the initial position to the injection point.
// The linear path starts at chaserStartManeuver (NOT pvtChaserInitial) and ends at injection point.
final List<TimeStampedPVCoordinates> linearWaypoints = rpoModel.computeForcedLinearWaypoints(chaserStartManeuver,
injectionWaypoint, 10);
// Compute the linear relative maneuvers to transfer the chaser to the injection point.
final List<RelativeManeuver> maneuvers = rpoModel.computeForcedManeuvers(linearWaypoints,
chaserStartManeuver.getVelocity(), targetOrbit, cwProvider);
for (RelativeManeuver maneuver: maneuvers) {
targetPropagator.addEventDetector(maneuver);
}
Now we need to compute the injection maneuver into the relative orbit. For that we must propagate the target orbit together with the chaser state provider in order to get the velocity of the chaser just before the injection maneuver at the injection point. Using this velocity before the maneuver it is possible to compute the ΔV to apply to inject the chaser in the relative orbit.
// Propagate the target orbit to the injection point in order to get the velocity of the chaser at the injection
// point just before the injection maneuver.
final SpacecraftState targetAtInjection = targetPropagator.propagate(INJECTION_DATE);
final double[] chaserBeforeInjection = cwProvider.getAdditionalData(targetAtInjection);
final Vector3D velocityBeforeInjection = new Vector3D(chaserBeforeInjection[3], chaserBeforeInjection[4], chaserBeforeInjection[5]);
// Compute the relative maneuver at injection date to insert the chaser in the relative natural orbit.
final Vector3D deltaV = injectionWaypoint.getVelocity().subtract(velocityBeforeInjection);
final RelativeManeuver injectionManeuver = new ClohessyWiltshireManeuver(new DateDetector(INJECTION_DATE), deltaV, cwProvider);
// Reset chaser initial PVT and propagator initial state.
cwProvider.setInitialChaserPVTLof(pvtChaserInitial);
targetPropagator.resetInitialState(new SpacecraftState(targetOrbit));
// Add the injection maneuver to the propagator.
targetPropagator.addEventDetector(injectionManeuver);
maneuvers.add(injectionManeuver);
Finally, the conversion from relative maneuvers to inertial maneuvers is performed to add them to the chaser independent propagator. It is now possible to propagate the target and the chaser.
// Convert all the relative maneuvers to impulse maneuvers and add them to the chaser propagator.
final List<ImpulseManeuver> impulseManeuvers = rpoModel.convertToImpulseManeuver(maneuvers, targetOrbit, 0);
for (ImpulseManeuver impulseManeuver: impulseManeuvers) {
chaserPropagator.addEventDetector(impulseManeuver);
}
// Propagate the target and the chaser to the end of the scenario.
targetPropagator.propagate(INJECTION_DATE.shiftedBy(3 * targetOrbit.getKeplerianPeriod()));
chaserPropagator.propagate(INJECTION_DATE.shiftedBy(3 * targetOrbit.getKeplerianPeriod()));
In the figure, Trajectory A is computed directly from the relative provider and Trajectory B is computed by the independent chaser propagator. Inclination is set to 0°.

If the inclination is set to 60° or -60°, the relative orbit will be naturally circular as shown on the following plot.

The complete code for this example can be found in the source tree of the tutorials, in file src/main/java/org/orekit/tutorials/control/relative/NaturalCircumnavigationPropagation.java.
Teardrop motion
Teardrop motion is a particular type of hovering motion close to a target when the target is in a circular orbit. This kind of trajectory has no analytical solution when the orbit of the target is eccentric.
From its initial position, the chaser follows a forced linear trajectory composed of multiple hops maneuvers before being inserted in the teardrop. The maneuvers are performed at intermediary waypoints defined by the user. The waypoints are points in time and space.
A teardrop is defined by two distances:
- turn_around_distance which is the distance from the target to the round end of the teardrop. The chaser is injected in the teardrop at this point.
- maneuver_distance which is the distance from the target to the pointy end of the teardrop. Maneuvers are performed at this point to make the chaser stay in the teardrop trajectory.
First thing is to compute the waypoints of the teardrop itself, composed of the injection waypoint at turn_around_distance from the target and the maneuvers waypoints at maneuver_distance from the target (one maneuver at the pointy end of the teardrop per teardrop motion).
// Compute the teardrop waypoints.
final List<TimeStampedPVCoordinates> teardropWaypoints = rpoModel.computeTeardropWaypoints(INJECTION_DATE,
targetOrbit, TURN_AROUND_DISTANCE, MANEUVER_DISTANCE, NUMBER_OF_TEARDROPS);
Then we can compute the waypoints and the associated relative maneuvers to linearly transfer the chaser satellite to the injection point of the teardrop:
// Definition of the linear path. The linear path starts at chaserStartManeuver (NOT pvtChaserInitial) and ends
// at injection point of the teardrop which is the first waypoint of the teardropWaypoints list.
final List<TimeStampedPVCoordinates> linearWaypoints = rpoModel.computeForcedLinearWaypoints(chaserStartManeuver, teardropWaypoints.get(0), 10);
// Creation of the maneuvers corresponding to the impulses of the linear transfer.
final List<RelativeManeuver> maneuvers = rpoModel.computeForcedManeuvers(linearWaypoints, chaserStartManeuver.getVelocity(), targetOrbit, cwProvider);
for (RelativeManeuver maneuver: maneuvers) {
targetPropagator.addEventDetector(maneuver);
}
Now that we can transfer the chaser to the injection point of the teardrop, it is necessary to compute the injection maneuver of the chaser into the teardrop. We already have the injection waypoint (first teardrop waypoint), whose velocity is the velocity of the chaser after the injection maneuver. The velocity at the injection point before the maneuver is needed to compute the required ΔV to inject the chaser in the teardrop:
// Propagate the target orbit to the injection point to get the velocity of the chaser at the injection point
// just before the injection maneuver.
final SpacecraftState targetAtInjection = targetPropagator.propagate(INJECTION_DATE);
final double[] chaserBeforeInjection = cwProvider.getAdditionalData(targetAtInjection);
final Vector3D velocityBeforeInjection = new Vector3D(chaserBeforeInjection[3], chaserBeforeInjection[4], chaserBeforeInjection[5]);
// Compute the relative maneuver at injection date to insert the chaser in the relative natural orbit.
final Vector3D deltaV = teardropWaypoints.get(0).getVelocity().subtract(velocityBeforeInjection);
final RelativeManeuver injectionManeuver = new ClohessyWiltshireManeuver(new DateDetector(INJECTION_DATE), deltaV, cwProvider);
// Add the injection maneuver to the propagator.
maneuvers.add(injectionManeuver);
targetPropagator.addEventDetector(injectionManeuver);
// Reset chaser initial PVT and propagator initial state.
cwProvider.setInitialChaserPVTLof(pvtChaserInitial);
targetPropagator.resetInitialState(new SpacecraftState(targetOrbit));
Finally, to compute the maneuvers to keep the chaser in the teardrop trajectory a simple call to the dedicated method is necessary:
// Create the maneuvers at the maneuver point of the teardrop and add them to the target propagator.
final List<RelativeManeuver> tearDropManeuvers = rpoModel.computeTeardropManeuvers(teardropWaypoints, cwProvider);
maneuvers.addAll(tearDropManeuvers);
for (RelativeManeuver maneuver: tearDropManeuvers) {
targetPropagator.addEventDetector(maneuver);
}
We can now convert all the relative maneuvers to ImpulseManeuver in the inertial frame and add them to the chaser independent propagator and then propagate the two spacecrafts.
// Convert all the relative maneuvers to impulse maneuvers and add them to the chaser propagator.
final List<ImpulseManeuver> impulseManeuvers = rpoModel.convertToImpulseManeuver(maneuvers, targetOrbit, 0);
for (ImpulseManeuver impulseManeuver: impulseManeuvers) {
chaserPropagator.addEventDetector(impulseManeuver);
}
// Propagate the target and the chaser to the end of the scenario.
targetPropagator.propagate(INJECTION_DATE.shiftedBy(NUMBER_OF_TEARDROPS * tearDropPeriod));
chaserPropagator.propagate(INJECTION_DATE.shiftedBy(NUMBER_OF_TEARDROPS * tearDropPeriod));
In the figure, Trajectory A is computed directly from the relative provider and Trajectory B is computed by the independent chaser propagator.

Zoom on the teardrop motion:

The complete code for this example can be found in the source tree of the tutorials, in file src/main/java/org/orekit/tutorials/control/relative/TearDropPropagation.java.
Co-elliptic orbit injection
Similarly to the natural circumnavigation orbit, it is possible to find an orbit in which the chaser will be relatively orbiting around the target when the latest is in any eccentric orbit. This type of orbit is called co-elliptic as it is very close to the target one.
From its initial position, the chaser follows a forced linear trajectory composed of multiple hops maneuvers before being inserted in the co-elliptic orbit. The maneuvers are performed at intermediary waypoints defined by the user. The waypoints are points in time and space.
To compute the co-elliptic orbit at injection date, target's orbital state is required. The target orbit is then propagated to injection date:
// Define target propagator and propagate target orbit to injection date.
final KeplerianPropagator targetPropagator = new KeplerianPropagator(targetOrbit);
final KeplerianOrbit targetAtInjection = (KeplerianOrbit) targetPropagator.propagate(INJECTION_DATE).getOrbit();
targetPropagator.resetInitialState(new SpacecraftState(targetOrbit));
It is now possible to compute the co-elliptic orbit of the chaser at injection date. The TimeStampedPVCoordinates of the chaser's orbit at injection date is
transformed to local orbital frame to get the injection waypoint of the chaser into the orbit:
// Compute the co-elliptic orbit at injection date.
final KeplerianOrbit coEllipticOrbit = CoellipticOrbit.computeChaserOrbit(targetAtInjection, SEMI_MINOR_AXIS,
X_PLANE_OFFSET, V_BAR_OFFSET, DRIFT_PER_ORBIT, PLANE_VECTOR_PHASE);
// Get Chaser PVT at injection.
final Transform inertialToLof = rpoModel.getLOFType().transformFromInertial(INJECTION_DATE, targetAtInjection.getPVCoordinates());
final TimeStampedPVCoordinates injectionWaypoint = inertialToLof.transformPVCoordinates(coEllipticOrbit.getPVCoordinates());
Then we can compute the waypoints and the associated relative maneuvers to linearly transfer the chaser satellite to the injection point of the co-elliptic orbit:
// Compute chaser linear waypoints to go linearly from the initial position to the injection point.
// The linear path starts at chaserStartManeuver (NOT pvtChaserInitial) and ends at injection point.
final List<TimeStampedPVCoordinates> linearWaypoints = rpoModel.computeForcedLinearWaypoints(chaserStartManeuver,
injectionWaypoint, 10);
// Compute the linear relative maneuvers to transfer the chaser to the injection point.
final List<RelativeManeuver> maneuvers = rpoModel.computeForcedManeuvers(linearWaypoints,
chaserStartManeuver.getVelocity(), targetOrbit, yaProvider);
for (RelativeManeuver maneuver: maneuvers) {
targetPropagator.addEventDetector(maneuver);
}
Now that we can transfer the chaser to the injection point of the co-elliptic orbit, it is necessary to compute the injection maneuver of the chaser into this orbit. We already have the injection waypoint, whose velocity is the velocity of the chaser after the injection maneuver. The velocity at the injection point before the maneuver is needed to compute the required ΔV to inject the chaser in the teardrop:
// Propagate the target orbit to the injection point in order to get the velocity of the chaser at the injection
// point just before the injection maneuver.
final SpacecraftState targetBeforeInjection = targetPropagator.propagate(INJECTION_DATE);
final double[] chaserBeforeInjection = yaProvider.getAdditionalData(targetBeforeInjection);
final Vector3D velocityBeforeInjection = new Vector3D(chaserBeforeInjection[3], chaserBeforeInjection[4], chaserBeforeInjection[5]);
// Compute the relative maneuver at injection date to insert the chaser in the relative natural orbit.
final Vector3D deltaV = injectionWaypoint.getVelocity().subtract(velocityBeforeInjection);
final RelativeManeuver injectionManeuver = new YamanakaAnkersenManeuver(new DateDetector(INJECTION_DATE), deltaV, yaProvider);
// Add the injection maneuver to the propagator.
targetPropagator.addEventDetector(injectionManeuver);
maneuvers.add(injectionManeuver);
Finally, we can convert the relative maneuvers to impulse maneuvers in inertial frame, add them to the chaser propagator and propagate the two spacecrafts' states.
// Convert all the relative maneuvers to impulse maneuvers and add them to the chaser propagator.
final List<ImpulseManeuver> impulseManeuvers = rpoModel.convertToImpulseManeuver(maneuvers, targetOrbit, 0);
for (ImpulseManeuver impulseManeuver: impulseManeuvers) {
chaserPropagator.addEventDetector(impulseManeuver);
}
// Propagate the target and the chaser to the end of the scenario.
targetPropagator.propagate(INJECTION_DATE.shiftedBy(3 * targetOrbit.getKeplerianPeriod()));
chaserPropagator.propagate(INJECTION_DATE.shiftedBy(3 * targetOrbit.getKeplerianPeriod()));
In the figures, Trajectory A is computed directly from the relative provider and Trajectory B is computed by the independent chaser propagator. It is important to note that a co-elliptic orbit is more stable at high altitude and low eccentricity. If the target is in a circular orbit and the desired co-elliptic orbit is not pimped with a center offset nor a drift per orbit, it is better to use the Natural Circumnavigation.
Semi-major axis: 36000km; eccentricity: 0.

Semi-major axis: 36000km; eccentricity: 0.05.

The complete code for this example can be found in the source tree of the tutorials, in file src/main/java/org/orekit/tutorials/control/relative/CoellipticOrbitPropagation.java.
CS GROUP
Orekit Tutorials