Lambert State Vectors
This example uses LambertIOD to solve Lambert's problem: given two position vectors and the time of flight between them, recover the full state vector (position plus velocity) with no SGP4 or TLE involved. That state can then feed numerical propagation, TLE generation, transfer planning, and multi-revolution trade studies.
import {
EpochUTC,
Kilometers,
KilometersPerSecond,
LambertIOD,
RungeKutta89Propagator,
Satellite,
Tle,
Vector3D,
} from 'ootk';Run it
npm run build
npx tsx ./examples/lambert-state-vector.tsBasic Lambert
lambert.estimate(p1, p2, t1, t2, options) returns a J2000 state at t1, or undefined when no solution converges, so always check the result. toClassicalElements() converts the state to orbital elements; the *Degrees getters avoid manual radian conversion and period is already in minutes.
/**
* Example 1: Basic Lambert Solution
* Generate a state vector from two position observations
*/
function example1BasicLambert(): void {
console.log('\n=== Example 1: Basic Lambert Solution ===\n');
// Two positions in ECI coordinates (km)
const p1 = new Vector3D<Kilometers>(6778.137 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const p2 = new Vector3D<Kilometers>(-2000.0 as Kilometers, 6400.0 as Kilometers, 1500.0 as Kilometers);
// Observation times
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T12:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T13:30:00.000Z'));
// Solve Lambert's problem
const lambert = new LambertIOD();
const stateVector = lambert.estimate(p1, p2, t1, t2, {
posigrade: true,
nRev: 0,
});
if (!stateVector) {
console.error('Lambert solution failed!');
return;
}
// Display state vector
console.log('State Vector at t1:');
console.log(' Epoch:', stateVector.epoch.toString());
console.log(' Position (km):', {
x: stateVector.position.x.toFixed(3),
y: stateVector.position.y.toFixed(3),
z: stateVector.position.z.toFixed(3),
});
console.log(' Velocity (km/s):', {
x: stateVector.velocity.x.toFixed(6),
y: stateVector.velocity.y.toFixed(6),
z: stateVector.velocity.z.toFixed(6),
});
// Convert to classical elements
const elements = stateVector.toClassicalElements();
console.log('\nClassical Orbital Elements:');
console.log(' Semi-major axis:', elements.semimajorAxis.toFixed(3), 'km');
console.log(' Eccentricity:', elements.eccentricity.toFixed(6));
console.log(' Inclination:', elements.inclinationDegrees.toFixed(3), 'deg');
console.log(' RAAN:', elements.rightAscensionDegrees.toFixed(3), 'deg');
console.log(' Arg of Perigee:', elements.argPerigeeDegrees.toFixed(3), 'deg');
console.log(' True Anomaly:', elements.trueAnomalyDegrees.toFixed(3), 'deg');
console.log(' Period:', elements.period.toFixed(2), 'minutes');
// Calculate apogee and perigee
const apogee = elements.semimajorAxis * (1 + elements.eccentricity);
const perigee = elements.semimajorAxis * (1 - elements.eccentricity);
console.log(' Apogee altitude:', (apogee - 6378.137).toFixed(3), 'km');
console.log(' Perigee altitude:', (perigee - 6378.137).toFixed(3), 'km');
}Lambert with propagator
The Lambert-derived J2000 state can seed RungeKutta89Propagator for high-precision numerical propagation, bypassing SGP4 entirely. propagate(epoch) integrates from the cached state to the requested epoch.
/**
* Example 2: Lambert + Numerical Propagator
* Use Lambert solution with high-precision propagation (no SGP4)
*/
function example2LambertWithPropagator(): void {
console.log('\n=== Example 2: Lambert + Numerical Propagator ===\n');
// Initial observation
const p1 = new Vector3D<Kilometers>(7000.0 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const p2 = new Vector3D<Kilometers>(0.0 as Kilometers, 7000.0 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T01:30:00.000Z'));
// Solve for orbit
const lambert = new LambertIOD();
const initialState = lambert.estimate(p1, p2, t1, t2);
if (!initialState) {
console.error('Lambert solution failed!');
return;
}
console.log('Initial state from Lambert:');
console.log(' Position magnitude:', initialState.position.magnitude().toFixed(3), 'km');
console.log(' Velocity magnitude:', initialState.velocity.magnitude().toFixed(6), 'km/s');
// Propagate using RungeKutta89 (high precision, no TLE required)
const propagator = new RungeKutta89Propagator(initialState);
// Propagate 1 day into future
const futureEpoch = EpochUTC.fromDateTime(new Date('2024-01-02T00:00:00.000Z'));
const futureState = propagator.propagate(futureEpoch);
console.log('\nState after 1 day propagation (RK89):');
console.log(' Position (km):', {
x: futureState.position.x.toFixed(3),
y: futureState.position.y.toFixed(3),
z: futureState.position.z.toFixed(3),
});
console.log(' Velocity (km/s):', {
x: futureState.velocity.x.toFixed(6),
y: futureState.velocity.y.toFixed(6),
z: futureState.velocity.z.toFixed(6),
});
// Compare with Keplerian expectations (period is in minutes)
const elements = initialState.toClassicalElements();
const periodMinutes = elements.period;
console.log('\nOrbit completed', ((24 * 60) / periodMinutes).toFixed(2), 'revolutions in 1 day');
}Lambert to Satellite
Tle.fromClassicalElements(elements) fits a TLE to the solution, which can then construct a Satellite object and use its convenience methods such as lla(date). Note that this treats osculating elements as mean elements, so SGP4 positions from the generated TLE will not exactly match the Lambert state.
/**
* Example 3: Convert Lambert Solution to Satellite Object
* Generate TLE from Lambert solution for use with Satellite class
*/
function example3LambertToSatellite(): void {
console.log('\n=== Example 3: Lambert to Satellite Conversion ===\n');
// Position observations
const p1 = new Vector3D<Kilometers>(6878.137 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers); // ~500 km altitude
const p2 = new Vector3D<Kilometers>(0.0 as Kilometers, 6878.137 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T01:30:00.000Z'));
// Solve Lambert
const lambert = new LambertIOD();
const j2000State = lambert.estimate(p1, p2, t1, t2);
if (!j2000State) {
console.error('Lambert solution failed!');
return;
}
// Convert to classical elements
const elements = j2000State.toClassicalElements();
console.log('Classical Elements from Lambert:');
console.log(' a:', elements.semimajorAxis.toFixed(3), 'km');
console.log(' e:', elements.eccentricity.toFixed(6));
console.log(' i:', elements.inclinationDegrees.toFixed(3), 'deg');
// Generate TLE
const tle = Tle.fromClassicalElements(elements);
console.log('\nGenerated TLE:');
console.log(' Line 1:', tle.line1);
console.log(' Line 2:', tle.line2);
// Create Satellite object
const satellite = new Satellite({
tle1: tle.line1,
tle2: tle.line2,
name: 'Lambert-Derived Satellite',
});
console.log('\nSatellite created from Lambert solution:');
console.log(' Name:', satellite.name);
console.log(' Inclination:', satellite.inclination.toFixed(3), 'deg');
console.log(' Period:', satellite.period.toFixed(2), 'minutes');
console.log(' Apogee:', satellite.apogee.toFixed(3), 'km');
console.log(' Perigee:', satellite.perigee.toFixed(3), 'km');
// Use Satellite methods
const futureDate = new Date('2024-01-01T06:00:00.000Z');
const lla = satellite.lla(futureDate);
console.log('\nSatellite position at +6 hours:');
console.log(' Latitude:', lla.lat.toFixed(3), 'deg');
console.log(' Longitude:', lla.lon.toFixed(3), 'deg');
console.log(' Altitude:', lla.alt.toFixed(3), 'km');
}Transfer orbit planning
Lambert's problem is the general form of transfer planning: fix the departure and arrival positions and the time of flight, and the solution's departure velocity minus the current orbital velocity is the first burn's delta-V vector.
/**
* Example 4: Transfer Orbit Planning
* Calculate delta-V for orbit transfer using Lambert
*/
function example4TransferOrbit(): void {
console.log('\n=== Example 4: Transfer Orbit Planning ===\n');
// Initial circular orbit (LEO)
const r1 = 6778.137; // km (400 km altitude)
const v1Circular = Math.sqrt(398600.4418 / r1); // Circular velocity
const pos1 = new Vector3D<Kilometers>(r1 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const vel1 = new Vector3D<KilometersPerSecond>(
0.0 as KilometersPerSecond,
v1Circular as KilometersPerSecond,
0.0 as KilometersPerSecond,
);
// Target circular orbit (MEO), 90 degrees ahead
const r2 = 12000.0; // km
const pos2 = new Vector3D<Kilometers>(0.0 as Kilometers, r2 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T05:00:00.000Z')); // 5-hour transfer
// Solve Lambert for transfer orbit
const lambert = new LambertIOD();
const transferOrbit = lambert.estimate(pos1, pos2, t1, t2);
if (!transferOrbit) {
console.error('Lambert solution failed!');
return;
}
// Calculate delta-V requirements
const deltaV1 = transferOrbit.velocity.subtract(vel1);
const deltaV1Magnitude = deltaV1.magnitude();
console.log('Transfer from LEO to MEO:');
console.log(' Initial orbit radius:', r1.toFixed(3), 'km');
console.log(' Target orbit radius:', r2.toFixed(3), 'km');
console.log(' Transfer time:', '5 hours');
console.log('\nTransfer orbit velocity at departure:');
console.log(' Required V:', transferOrbit.velocity.magnitude().toFixed(6), 'km/s');
console.log(' Current V:', v1Circular.toFixed(6), 'km/s');
console.log(' Delta-V1:', deltaV1Magnitude.toFixed(6), 'km/s');
console.log(' Delta-V1 (m/s):', (deltaV1Magnitude * 1000).toFixed(2), 'm/s');
// Transfer orbit characteristics
const elements = transferOrbit.toClassicalElements();
console.log('\nTransfer orbit elements:');
console.log(' Semi-major axis:', elements.semimajorAxis.toFixed(3), 'km');
console.log(' Eccentricity:', elements.eccentricity.toFixed(6));
console.log(' Apogee:', (elements.semimajorAxis * (1 + elements.eccentricity)).toFixed(3), 'km');
console.log(' Perigee:', (elements.semimajorAxis * (1 - elements.eccentricity)).toFixed(3), 'km');
}Multi revolution
For long flight times the transfer can complete whole revolutions before arrival. The nRev option selects the number of complete revolutions; higher nRev solutions here need less departure velocity at the cost of a more constrained geometry.
/**
* Example 5: Multi-Revolution Comparison
* Compare different revolution options for same transfer
*/
function example5MultiRevolution(): void {
console.log('\n=== Example 5: Multi-Revolution Transfers ===\n');
const p1 = new Vector3D<Kilometers>(7000.0 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const p2 = new Vector3D<Kilometers>(0.0 as Kilometers, 8000.0 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T06:00:00.000Z'));
const lambert = new LambertIOD();
console.log('Comparing transfer options (6-hour time of flight):');
console.log('From:', { x: p1.x, y: p1.y, z: p1.z }, 'km');
console.log('To:', { x: p2.x, y: p2.y, z: p2.z }, 'km\n');
// Try 0, 1, and 2 complete revolutions
for (let nRev = 0; nRev <= 2; nRev++) {
const solution = lambert.estimate(p1, p2, t1, t2, {
posigrade: true,
nRev,
});
if (solution) {
const elements = solution.toClassicalElements();
const vMag = solution.velocity.magnitude();
console.log(`${nRev}-Revolution Transfer:`);
console.log(' Departure velocity:', vMag.toFixed(6), 'km/s');
console.log(' Semi-major axis:', elements.semimajorAxis.toFixed(3), 'km');
console.log(' Eccentricity:', elements.eccentricity.toFixed(6));
console.log(' Period:', elements.period.toFixed(2), 'minutes');
console.log('');
} else {
console.log(`${nRev}-Revolution: No solution\n`);
}
}
}Short vs long path
The posigrade flag chooses which way around the central body the transfer sweeps. Between nearly opposite points the two paths converge toward the same half-orbit geometry, so their delta-V difference is small.
/**
* Example 6: Short Path vs Long Path
* Demonstrate the difference between short and long path transfers
*/
function example6PathComparison(): void {
console.log('\n=== Example 6: Short Path vs Long Path ===\n');
const p1 = new Vector3D<Kilometers>(7000.0 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const p2 = new Vector3D<Kilometers>(-7000.0 as Kilometers, 1000.0 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T02:00:00.000Z'));
const lambert = new LambertIOD();
// Short path (posigrade = true)
const shortPath = lambert.estimate(p1, p2, t1, t2, { posigrade: true });
// Long path (posigrade = false)
const longPath = lambert.estimate(p1, p2, t1, t2, { posigrade: false });
console.log('Transfer between nearly opposite points:\n');
if (shortPath) {
const elementsShort = shortPath.toClassicalElements();
console.log('Short Path Transfer:');
console.log(' Velocity:', shortPath.velocity.magnitude().toFixed(6), 'km/s');
console.log(' Semi-major axis:', elementsShort.semimajorAxis.toFixed(3), 'km');
console.log(' Eccentricity:', elementsShort.eccentricity.toFixed(6));
console.log('');
}
if (longPath) {
const elementsLong = longPath.toClassicalElements();
console.log('Long Path Transfer:');
console.log(' Velocity:', longPath.velocity.magnitude().toFixed(6), 'km/s');
console.log(' Semi-major axis:', elementsLong.semimajorAxis.toFixed(3), 'km');
console.log(' Eccentricity:', elementsLong.eccentricity.toFixed(6));
}
if (shortPath && longPath) {
const deltaVDiff = Math.abs(shortPath.velocity.magnitude() - longPath.velocity.magnitude());
console.log('\nDifference:');
console.log(' Delta-V difference:', (deltaVDiff * 1000).toFixed(2), 'm/s');
}
}Validation
estimate() returning undefined is the failure signal, but a returned solution can still be physically useless: very short flight times force hyperbolic-scale velocities, and the resulting ellipse may dip below the Earth's surface. Check perigee radius and eccentricity before trusting a solution.
/**
* Example 7: Validation and Error Checking
* Demonstrate proper error handling and solution validation
*/
function example7Validation(): void {
console.log('\n=== Example 7: Validation and Error Checking ===\n');
const lambert = new LambertIOD();
// Test Case 1: Valid solution
console.log('Test 1: Valid transfer');
const p1 = new Vector3D<Kilometers>(7000.0 as Kilometers, 0.0 as Kilometers, 0.0 as Kilometers);
const p2 = new Vector3D<Kilometers>(0.0 as Kilometers, 7000.0 as Kilometers, 0.0 as Kilometers);
const t1 = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:00.000Z'));
const t2 = EpochUTC.fromDateTime(new Date('2024-01-01T01:30:00.000Z'));
let solution = lambert.estimate(p1, p2, t1, t2);
if (solution) {
console.log(' [ok] Solution found');
console.log(' Velocity:', solution.velocity.magnitude().toFixed(6), 'km/s\n');
} else {
console.log(' [fail] Solution failed\n');
}
// Test Case 2: Very short time of flight
console.log('Test 2: Very short time of flight (30 seconds)');
const t2Short = EpochUTC.fromDateTime(new Date('2024-01-01T00:00:30.000Z'));
solution = lambert.estimate(p1, p2, t1, t2Short);
if (solution) {
console.log(' [ok] Solution found (hyperbolic velocity required)');
console.log(' Velocity:', solution.velocity.magnitude().toFixed(6), 'km/s');
} else {
console.log(' [fail] Solution failed, time of flight too short\n');
}
// Test Case 3: Positions too close
console.log('Test 3: Positions very close together');
const p2Close = new Vector3D<Kilometers>(7000.1 as Kilometers, 0.1 as Kilometers, 0.0 as Kilometers);
solution = lambert.estimate(p1, p2Close, t1, t2);
if (solution) {
console.log(' [ok] Solution found\n');
} else {
console.log(' [fail] Solution failed, positions too close\n');
}
// Test Case 4: Validate orbital parameters
console.log('Test 4: Solution validation');
solution = lambert.estimate(p1, p2, t1, t2);
if (solution) {
const elements = solution.toClassicalElements();
const earthRadius = 6378.137;
console.log(' Checking orbital parameters:');
// Check if orbit intersects Earth
const perigeeRadius = elements.semimajorAxis * (1 - elements.eccentricity);
if (perigeeRadius < earthRadius) {
console.log(' [warn] WARNING: Orbit intersects Earth!');
console.log(' Perigee radius:', perigeeRadius.toFixed(3), 'km');
} else {
console.log(' [ok] Orbit is valid (perigee above surface)');
console.log(' Perigee altitude:', (perigeeRadius - earthRadius).toFixed(3), 'km');
}
// Check for hyperbolic orbit
if (elements.eccentricity >= 1.0) {
console.log(' [warn] Hyperbolic trajectory (e >= 1.0)');
} else {
console.log(' [ok] Elliptical orbit (e < 1.0)');
}
}
}Run all
function main(): void {
console.log('\nLambert State Vector Generation Examples');
console.log('Generating state vectors without SGP4/TLE');
example1BasicLambert();
example2LambertWithPropagator();
example3LambertToSatellite();
example4TransferOrbit();
example5MultiRevolution();
example6PathComparison();
example7Validation();
console.log('\nAll examples completed!\n');
}
main();Output
Lambert State Vector Generation Examples
Generating state vectors without SGP4/TLE
=== Example 1: Basic Lambert Solution ===
State Vector at t1:
Epoch: 2024-01-01T12:00:00.000Z
Position (km): { x: '6778.137', y: '0.000', z: '0.000' }
Velocity (km/s): { x: '5.696659', y: '5.710923', z: '1.338498' }
Classical Orbital Elements:
Semi-major axis: 7853.355 km
Eccentricity: 0.703585
Inclination: 13.191 deg
RAAN: 0.000 deg
Arg of Perigee: 233.862 deg
True Anomaly: 126.138 deg
Period: 115.44 minutes
Apogee altitude: 7000.717 km
Perigee altitude: -4050.281 km
=== Example 2: Lambert + Numerical Propagator ===
Initial state from Lambert:
Position magnitude: 7000.000 km
Velocity magnitude: 7.928951 km/s
State after 1 day propagation (RK89):
Position (km): { x: '6488.321', y: '10781.861', z: '0.000' }
Velocity (km/s): { x: '-3.498089', y: '-0.313370', z: '0.000000' }
Orbit completed 12.57 revolutions in 1 day
=== Example 3: Lambert to Satellite Conversion ===
Classical Elements from Lambert:
a: 7775.497 km
e: 0.774509
i: 0.000 deg
Generated TLE:
Line 1: 1 00001U 58001A 24001.00000000 .00000000 00000+0 00000+0 0 9990
Line 2: 2 00001 0.0000 0.0000 7745093 225.0000 37.5498 12.66223876 06
Satellite created from Lambert solution:
Name: Lambert-Derived Satellite
Inclination: 0.000 deg
Period: 113.72 minutes
Apogee: 7426.696 km
Perigee: -4617.697 km
... (truncated)
=== Example 6: Short Path vs Long Path ===
Transfer between nearly opposite points:
Short Path Transfer:
Velocity: 8.435877 km/s
Semi-major axis: 9330.136 km
Eccentricity: 0.523382
Long Path Transfer:
Velocity: 8.435857 km/s
Semi-major axis: 9330.065 km
Eccentricity: 0.469926
Difference:
Delta-V difference: 0.02 m/s
=== Example 7: Validation and Error Checking ===
Test 1: Valid transfer
[ok] Solution found
Velocity: 7.928951 km/s
Test 2: Very short time of flight (30 seconds)
[ok] Solution found (hyperbolic velocity required)
Velocity: 329.940694 km/s
Test 3: Positions very close together
[ok] Solution found
Test 4: Solution validation
Checking orbital parameters:
[warn] WARNING: Orbit intersects Earth!
Perigee radius: 1805.850 km
[ok] Elliptical orbit (e < 1.0)
All examples completed!