Click or drag to resize

Lambert and Primer Vector Example

This topic illustrates the use of the LambertOrbitSolver to calculate delta-v vectors for maneuvers and the use of the a PropagationVector as a costate vector to target the directions of maneuvers with known delta-v magnitudes. Primer vector theory is applied to propagate the costate vector such that it points in the correct direction for both the initial and final maneuvers of a coplanar low Earth orbit (LEO) to geostationary Earth orbit (GEO) transfer.

Primer vectors were introduced by D. F. Lawden in "Optimal Trajectories for Space Navigation", published by Butterworths in 1963. Some of the equations and methodology used in this discussion are provided by J. E. Prussing in Chapter 2 "Primer Vector Theory and Applications" of "Spacecraft Trajectory Optimization", edited by B. A. Conway and published by Cambridge University Press in 2010.

Setup and Definition of Orbits

First, some preliminary setup needs to be completed. The reference frame, central body, central body radius, and central body gravitational parameter need to be defined to use the International Celestial Reference Frame (ICRF) and Earth-specific values. The initial date (or epoch) must be defined for the propagation definition to work, but its specific value is not particularly relevant to this application.

The LEO is defined as an equatorial circular orbit with an altitude of 300 km, and the GEO is defined using position and velocity vectors to begin 180 degrees away from the LEO starting conditions. The semi-major axis and time of flight of the geostationary transfer orbit (GTO) are defined to be consistent with an optimal Hohmann transfer from the LEO to the GEO.

Java
// We want orbits with less than 1 complete revolution.
final int numberOfRevolutions = 0;

// Define central body parameters, reference frame, and initial date.
EarthCentralBody earth = CentralBodiesFacet.getFromContext().getEarth();
ReferenceFrame icrfFrame = earth.getInternationalCelestialReferenceFrame();
double gravitationalParameter = EarthGravitationalModel2008.GravitationalParameter; // 3.986004415e+14 m^3/s^2
double earthRadius = EarthGravitationalModel2008.SemimajorAxis; // 6378136.3 meters
JulianDate initialDate = new GregorianDate(2022, 3, 16).toJulianDate();

// Parameters specific to initial LEO.
double initialSemimajorAxis = 300000.0 + earthRadius; // Circular orbit with 300 km altitude.
double initialEccentricity = 0.0;
double initialInclination = 0.0;
double initialRightAscensionAscendingNode = 0.0;
double initialArgumentOfPerigee = 0.0;
double initialTrueAnomaly = 0.0;
Motion1<Cartesian> initialLeo = new KeplerianElements(initialSemimajorAxis, initialEccentricity,
        initialInclination, initialArgumentOfPerigee, initialRightAscensionAscendingNode,
        initialTrueAnomaly, gravitationalParameter).toCartesian();

// Parameters specific to final GEO.
Motion1<Cartesian> finalGeo = new Motion1<Cartesian>(new Cartesian(-42165000.0, 0.0, 0.0), // position vector, meters.
        new Cartesian(0.0, -3074.6, 0.0)); // velocity vector, m/s.
// Time of flight of GTO transfer orbit.
double gtoSemimajorAxis = (initialSemimajorAxis + finalGeo.getValue().getMagnitude()) / 2.0; // meters.
double gtoTimeOfFlight = 0.5 * OrbitalElements.computePeriod(1.0 / gtoSemimajorAxis, gravitationalParameter); // seconds.
Solving for Initial Primer Vector Rate

The LambertOrbitSolver is used to compute the initial and final velocity vectors for the GTO. Because the GTO is a 180 degree transfer, there is an inherent ambiguity in the orbital plane of a transfer between the initial and final locations. The velocity vector of the initial LEO orbit is used to resolve that ambiguity and ensure a zero-inclination GTO.

The delta-v vector of the first maneuver is computed by subtracting the initial LEO velocity vector from the initial velocity vector of the Lambert solution. Similarly, the delta-v vector of the second maneuver is computed by subtracting the final velocity vector of the Lambert solution from the velocity vector of the desired GEO orbit.

The initial and final values of the primer vectors are found by normalizing the delta-v vectors for the first and second maneuvers. In most trajectory optimization applications, the initial primer vector values would have to be guessed and corrected. However, this simple example uses a Hohmann transfer that is already known to be optimal.

The initial value of the first derivative of the primer vector (also known as the primer vector rate) can be calculated from the initial and final primer vectors, but this calculation requires the state transition matrix (STM) to be computed first. A separate propagator is configured that allows the computation of the STM from the initial state to the final state of the GTO. The STM is a 6x6 matrix because both the position and velocity vectors are included. However, only two 3x3 sub-matrices of the STM are needed to compute the initial primer vector rate. (See equations 2.36, 2.37, 2.46, and 2.49 of Prussing for details.)

Java
// Find solution using Lambert solver.
LambertOrbitSolver lambertSolver = new LambertOrbitSolver(gravitationalParameter);
LambertResult lambertSolution = lambertSolver.solveFixedDurationTransfer(initialLeo.getValue(), finalGeo.getValue(),
        Duration.fromSeconds(gtoTimeOfFlight), numberOfRevolutions, LambertPathType.SHORT_PATH,
        OrbitDirectionType.PROGRADE, initialLeo.getFirstDerivative());

// Calculate delta-v's required to complete Lambert solution.
Cartesian deltaV1 = Cartesian.subtract(lambertSolution.getInitialPositionMotion().getFirstDerivative(), initialLeo.getFirstDerivative());
Cartesian deltaV2 = Cartesian.subtract(finalGeo.getFirstDerivative(), lambertSolution.getFinalPositionMotion().getFirstDerivative());

double deltaV1Magnitude = deltaV1.getMagnitude();
double deltaV2Magnitude = deltaV2.getMagnitude();
UnitCartesian initialPrimerVector = deltaV1.normalize();
UnitCartesian finalPrimerVector = deltaV2.normalize();

// Setup preliminary propagator to calculate state transition matrix.
NumericalPropagatorDefinition stmPropagatorDefinition = new NumericalPropagatorDefinition();
stmPropagatorDefinition.setEpoch(initialDate);

double initialMass = 1000.0; // kg.
String massElementId = "Mass";
PropagationScalar mass = new PropagationScalar(initialMass);
mass.setIdentification(massElementId);
mass.setScalarDerivative(Scalar.toScalar(0.0));

// Define states using PropagationNewtonianPoint.
final String id = "PrimaryMotion";
PropagationNewtonianPoint point = new PropagationNewtonianPoint();
point.setIdentification(id);
point.setMass(mass.getIntegrationValue());
point.setInitialPosition(initialLeo.getValue());
point.setInitialVelocity(Cartesian.add(initialLeo.getFirstDerivative(), deltaV1));
point.setIntegrationFrame(icrfFrame);

// Configure two-body gravity.
TwoBodyGravity twoBodyGravity = new TwoBodyGravity(point.getIntegrationPoint(), earth, gravitationalParameter);
point.getAppliedForces().add(twoBodyGravity);
stmPropagatorDefinition.getIntegrationElements().add(mass);
stmPropagatorDefinition.getIntegrationElements().add(point);

// Create state transition matrix and add it as an integration element.
final String stmId = "StateTransitionMatrix";
IPartialDifferentiable position = (IPartialDifferentiable)point.getIntegrationPoint();
IPartialDifferentiable velocity = (IPartialDifferentiable)point.getIntegrationPoint().createVectorVelocity(point.getIntegrationFrame());
// point is the IPartialDifferentiable for the acceleration.

StateTransitionMatrix stateMatrix = new StateTransitionMatrix();
stateMatrix.setIdentification(stmId);
stateMatrix.addStateParameter(position, velocity);
stateMatrix.addStateParameter(velocity, point);
stmPropagatorDefinition.getIntegrationElements().add(stateMatrix);

// Configure numerical integrator.
RungeKuttaFehlberg78Integrator integrator = new RungeKuttaFehlberg78Integrator();
integrator.setRelativeTolerance(Constants.Epsilon13);
integrator.setMinimumStepSize(1.0);
integrator.setMaximumStepSize(6.0 * 3600.0);
integrator.setInitialStepSize(60.0);
integrator.setAbsoluteTolerance(Constants.Epsilon13);
integrator.setStepSizeBehavior(KindOfStepSize.RELATIVE);
stmPropagatorDefinition.setIntegrator(integrator);

// Propagate and extract results.
NumericalPropagator stmPropagator = stmPropagatorDefinition.createPropagator();
NumericalPropagationStateHistory stmPropagatorResults = stmPropagator.propagate(Duration.fromSeconds(gtoTimeOfFlight), 1);
List<Matrix> stmHistory = stmPropagatorResults.<Matrix> getDateMotionCollection(stmId).getValues();
int stmHistoryLength = stmHistory.size();
Matrix finalStm = stmHistory.get(stmHistoryLength - 1);

// Extract dPosition/dPosition0 submatrix.
Matrix finalStmSubmatrix1 = finalStm.getMatrix(0, 3, 0, 3);
Matrix3By3 convertedSubmatrix1 =
        new Matrix3By3(finalStmSubmatrix1.get(0, 0), finalStmSubmatrix1.get(0, 1), finalStmSubmatrix1.get(0, 2),
                       finalStmSubmatrix1.get(1, 0), finalStmSubmatrix1.get(1, 1), finalStmSubmatrix1.get(1, 2),
                       finalStmSubmatrix1.get(2, 0), finalStmSubmatrix1.get(2, 1), finalStmSubmatrix1.get(2, 2));

// Extract dPosition/dVelocity0 submatrix.
Matrix finalStmSubmatrix2 = finalStm.getMatrix(0, 3, 3, 3);
Matrix3By3 convertedSubmatrix2 =
        new Matrix3By3(finalStmSubmatrix2.get(0, 0), finalStmSubmatrix2.get(0, 1), finalStmSubmatrix2.get(0, 2),
                       finalStmSubmatrix2.get(1, 0), finalStmSubmatrix2.get(1, 1), finalStmSubmatrix2.get(1, 2),
                       finalStmSubmatrix2.get(2, 0), finalStmSubmatrix2.get(2, 1), finalStmSubmatrix2.get(2, 2));

// Solve for initial primer vector rate.
Cartesian rightHandSide = finalPrimerVector.subtract(
        Matrix3By3.multiply(convertedSubmatrix1, Cartesian.toCartesian(initialPrimerVector)));
Cartesian initialPrimerVectorRate = Matrix3By3.multiply(convertedSubmatrix2.invert(), rightHandSide);
Main propagator and segment definitions

The primer vector is defined as a costate PropagationVector that uses the pre-computed initial primer vector and initial primer vector rates as its initial conditions. The VectorDerivative (get / set), which is the second derivative of the primer vector in this example, is defined by multiplying the instantaneous value of the gravity-gradient matrix of the Earth's two-body gravity by the instantaneous value of primer vector. Scalar arithmetic is used to describe these computations in order to delay the execution of these operations until the propagator actually runs.

Next, the main propagator is configured with the constant mass, spacecraft point, and primer vector as PropagationStateElements. The magnitudes of the maneuvers were calculated earlier in the code from the Lambert solutions. The directions of the maneuvers are defined by the primer vector values that occur immediately before each maneuver.

Finally, a SegmentList is configured that uses an initial state segment, a first maneuver segment, a coast segment, and a second maneuver segment.

Java
// Now set-up main propagator that includes primer-vector propagation and maneuvers.
point.setInitialVelocity(initialLeo.getFirstDerivative()); // Needs to be reset to no longer include deltaV1.

// Define PrimerVector as a costate PropagationVector
String primerVectorId = "PrimerVector";
PropagationVector primerVector = new PropagationVector(Cartesian.toCartesian(initialPrimerVector), initialPrimerVectorRate);
primerVector.setIdentification(primerVectorId);
primerVector.setPropagationAxes(icrfFrame.getAxes());
// The VectorDerivative is the second derivative and is set in the following code.

// Get partial derivatives of the gravity.
Scalar radius = point.getIntegrationPoint().getScalarElement(CartesianElement.MAGNITUDE, point.getIntegrationFrame());
Scalar radiusX = point.getIntegrationPoint().getScalarElement(CartesianElement.X, point.getIntegrationFrame());
Scalar radiusY = point.getIntegrationPoint().getScalarElement(CartesianElement.Y, point.getIntegrationFrame());
Scalar radiusZ = point.getIntegrationPoint().getScalarElement(CartesianElement.Z, point.getIntegrationFrame());
Scalar radiusSquared = Scalar.multiply(radius, radius);
Scalar factor = Scalar.divide(gravitationalParameter, (Scalar.multiply(radiusSquared, radius)));

// Two-body gravity has simple partial derivatives. Even then, these operations only work because
// all of the states and co-states are in the same reference frame.
Scalar dGravityX_dX = Scalar.multiply(factor, (Scalar.subtract(Scalar.divide(Scalar.multiply(
        Scalar.multiply(3.0, radiusX), radiusX), radiusSquared), Scalar.toScalar(1.0))));
Scalar dGravityY_dY = Scalar.multiply(factor, (Scalar.subtract(Scalar.divide(Scalar.multiply(
        Scalar.multiply(3.0, radiusY), radiusY), radiusSquared), Scalar.toScalar(1.0))));
Scalar dGravityZ_dZ = Scalar.multiply(factor, (Scalar.subtract(Scalar.divide(Scalar.multiply(
        Scalar.multiply(3.0, radiusZ), radiusZ), radiusSquared), Scalar.toScalar(1.0))));
// Equal to dGravityY_dX
Scalar dGravityX_dY = Scalar.divide(Scalar.multiply(Scalar.multiply(Scalar.multiply(factor, 3.0), radiusX), radiusY), radiusSquared);
// Equal to dGravityZ_dX
Scalar dGravityX_dZ = Scalar.divide(Scalar.multiply(Scalar.multiply(Scalar.multiply(factor, 3.0), radiusX), radiusZ), radiusSquared);
// Equal to dGravityZ_dY
Scalar dGravityY_dZ = Scalar.divide(Scalar.multiply(Scalar.multiply(Scalar.multiply(factor, 3.0), radiusY), radiusZ), radiusSquared);

// Primer vector is multiplied by partials matrix to get derivatives.
Scalar primerVectorX = primerVector.getIntegrationValue().getScalarElement(CartesianElement.X, icrfFrame.getAxes());
Scalar primerVectorY = primerVector.getIntegrationValue().getScalarElement(CartesianElement.Y, icrfFrame.getAxes());
Scalar primerVectorZ = primerVector.getIntegrationValue().getScalarElement(CartesianElement.Z, icrfFrame.getAxes());
Scalar primerSecondDerivativeX = Scalar.add(Scalar.add(Scalar.multiply(primerVectorX, dGravityX_dX),
                                                       Scalar.multiply(primerVectorY, dGravityX_dY)),
                                                       Scalar.multiply(primerVectorZ, dGravityX_dZ));
Scalar primerSecondDerivativeY = Scalar.add(Scalar.add(Scalar.multiply(primerVectorX, dGravityX_dY),
                                                       Scalar.multiply(primerVectorY, dGravityY_dY)),
                                                       Scalar.multiply(primerVectorZ, dGravityY_dZ));
Scalar primerSecondDerivativeZ = Scalar.add(Scalar.add(Scalar.multiply(primerVectorX, dGravityX_dZ),
                                                       Scalar.multiply(primerVectorY, dGravityY_dZ)),
                                                       Scalar.multiply(primerVectorZ, dGravityZ_dZ));
Vector primerSecondDerivative = Vector.add(
        Vector.add(Vector.multiply(primerSecondDerivativeX, VectorFixed.getUnitXVector(icrfFrame.getAxes())),
                Vector.multiply(primerSecondDerivativeY, VectorFixed.getUnitYVector(icrfFrame.getAxes()))),
        Vector.multiply(primerSecondDerivativeZ, VectorFixed.getUnitZVector(icrfFrame.getAxes())));
primerVector.setVectorDerivative(primerSecondDerivative);

// Set-up main propagator.
NumericalPropagatorDefinition mainPropagatorDefinition = new NumericalPropagatorDefinition();
mainPropagatorDefinition.setEpoch(initialDate);
mainPropagatorDefinition.setIntegrator(integrator);
mainPropagatorDefinition.getIntegrationElements().add(mass);
mainPropagatorDefinition.getIntegrationElements().add(point);
mainPropagatorDefinition.getIntegrationElements().add(primerVector);

// Set-up segment propagation.
NumericalInitialStateSegment initial = new NumericalInitialStateSegment();
initial.setName("Initial State");
initial.setPropagatorDefinition(mainPropagatorDefinition);

// To illustrate the use of primer vectors, the delta-v will be purely in the x-direction of the maneuver's orientation axes.
ImpulsiveManeuverSegment maneuver1 = new ImpulsiveManeuverSegment();
maneuver1.setManeuver(new ImpulsiveManeuverInformation(id, new Cartesian(deltaV1Magnitude, 0.0, 0.0)));
maneuver1.getManeuver().setCostateElementIdentification(primerVectorId);

// The maneuver's orientation axes are defined using the costate vector of the maneuver, which is the primer vector.
AxesAlignedConstrained axes1 = new AxesAlignedConstrained(maneuver1.getManeuver().getCostateVector(),
        new VectorTrueDisplacement(earth.getCenterOfMassPoint(), maneuver1.getManeuver().getPropagationPoint()));
maneuver1.getManeuver().setOrientation(axes1);

// The GTO transfer orbit is defined next.
NumericalPropagatorSegment gtoSegment = new NumericalPropagatorSegment();
gtoSegment.setName("GTO coast segment");
gtoSegment.setPropagatorDefinition(mainPropagatorDefinition);
gtoSegment.setMaximumDuration(Duration.fromDays(1.0));

// Stop after the GTO is finished.
DurationStoppingCondition durationStop = new DurationStoppingCondition(Duration.fromSeconds(gtoTimeOfFlight));
gtoSegment.getStoppingConditions().add(durationStop);

// To illustrate the use of primer vectors, the delta-v will be purely in the x-direction of the maneuver's orientation axes.
ImpulsiveManeuverSegment maneuver2 = new ImpulsiveManeuverSegment();
maneuver2.setManeuver(new ImpulsiveManeuverInformation(id, new Cartesian(deltaV2Magnitude, 0.0, 0.0)));
maneuver2.getManeuver().setCostateElementIdentification(primerVectorId);

// The maneuver's orientation axes are defined using the costate vector of the maneuver, which is the primer vector.
AxesAlignedConstrained axes2 = new AxesAlignedConstrained(maneuver2.getManeuver().getCostateVector(),
        new VectorTrueDisplacement(earth.getCenterOfMassPoint(), maneuver2.getManeuver().getPropagationPoint()));
maneuver2.getManeuver().setOrientation(axes2);

// Make a segment list, populate the segment list, and propagate the segment list.
SegmentList mainSegmentList = new SegmentList();
mainSegmentList.getSegments().add(initial);
mainSegmentList.getSegments().add(maneuver1);
mainSegmentList.getSegments().add(gtoSegment);
mainSegmentList.getSegments().add(maneuver2);
Propagation and Results

A SegmentListPropagator is generated from the SegmentList and then propagated to get results. The results for each of the four inner segments are extracted from the SegmentListResults.

The initial and final values of the position, velocity, and costate primer vectors of the spacecraft are extracted from the results of each relevant segment.

Java
// Define propagator and propagate it.
SegmentListPropagator mainPropagator = mainSegmentList.getSegmentListPropagator();
SegmentListResults mainResults = mainPropagator.propagateSegmentList();

// Extract results for each segment inside segment list.
SegmentResults initialSegmentResults = mainResults.getResultsOfSegment(initial);
ImpulsiveManeuverSegmentResults maneuver1Results = (ImpulsiveManeuverSegmentResults) mainResults.getResultsOfSegment(maneuver1);
SegmentResults gtoSegmentResults = mainResults.getResultsOfSegment(gtoSegment);
ImpulsiveManeuverSegmentResults maneuver2Results = (ImpulsiveManeuverSegmentResults) mainResults.getResultsOfSegment(maneuver2);

Motion1<Cartesian> propagationInitialState = initialSegmentResults.getFirstPropagatedState().<Cartesian> getMotion(id);

Motion1<Cartesian> maneuver1InitialState = maneuver1Results.getFirstPropagatedState().<Cartesian> getMotion(id);
Motion1<Cartesian> maneuver1FinalState = maneuver1Results.getFinalPropagatedState().<Cartesian> getMotion(id);

Motion1<Cartesian> gtoInitialState = gtoSegmentResults.getFirstPropagatedState().<Cartesian> getMotion(id);
Motion1<Cartesian> gtoFinalState = gtoSegmentResults.getFinalPropagatedState().<Cartesian> getMotion(id);

Motion1<Cartesian> gtoInitialCostate = gtoSegmentResults.getFirstPropagatedState().<Cartesian> getMotion(primerVectorId);
Motion1<Cartesian> gtoFinalCostate = gtoSegmentResults.getFinalPropagatedState().<Cartesian> getMotion(primerVectorId);

Motion1<Cartesian> maneuver2InitialState = maneuver2Results.getFirstPropagatedState().<Cartesian> getMotion(id);
Motion1<Cartesian> maneuver2FinalState = maneuver2Results.getFinalPropagatedState().<Cartesian> getMotion(id);