UNPKG

keplerian-walkerdelta-propagator

Version:

A JavaScript library for two-body Keplerian orbital propagation and Walker Delta constellation generation

179 lines (160 loc) 7.05 kB
// # Keplerian Constellation Propagator // # Copyright(C) 2025 Ralph M.C.Ralph // # // # This program is free software: you can redistribute it and / or modify // # it under the terms of the GNU General Public License as published by // # the Free Software Foundation, either version 3 of the License, or // #(at your option) any later version. // # // # This program is distributed in the hope that it will be useful, // # but WITHOUT ANY WARRANTY; without even the implied warranty of // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the // # GNU General Public License for more details. // # // # You should have received a copy of the GNU General Public License // # along with this program.If not, see < https://www.gnu.org/licenses/>. // keplerianPropagator.js class KeplerianPropagator { constructor() { // Physical constants this.mu = 3.986004418e14; // m³/s² (exact value used in MATLAB) } /** * Generates a Walker Delta constellation * @param {Object} params - Constellation parameters * @param {number} params.a - Semi-major axis in meters * @param {number} params.i - Inclination in degrees * @param {number} params.totalSatellites - Total number of satellites * @param {number} params.geometryPlanes - Number of orbital planes * @param {number} params.phasing - Phasing factor * @param {number} params.epoch - Initial epoch in milliseconds * @returns {Array} Array of objects with orbital parameters for each satellite */ generateWalkerDelta({ a, i, totalSatellites, geometryPlanes, phasing, epoch }) { const sats = []; const satsPerPlane = totalSatellites / geometryPlanes; const deltaRAAN = 360 / geometryPlanes; const deltaAnomaly = 360 / satsPerPlane; const phaseShift = (phasing * 360) / totalSatellites; for (let p = 0; p < geometryPlanes; p++) { const raan = p * deltaRAAN; for (let s = 0; s < satsPerPlane; s++) { const ν = (s * deltaAnomaly + p * phaseShift) % 360; sats.push({ a, e: 0, i, raan, argPerigee: 0, trueAnomaly0: ν, epoch, name: `p${p + 1}s${s + 1}` }); } } return sats; } /** * Propagates a satellite using Keplerian dynamics * @param {Object} params - Orbital parameters * @param {number} params.a - Semi-major axis in meters * @param {number} params.e - Eccentricity * @param {number} params.i - Inclination in degrees * @param {number} params.raan - Right ascension of ascending node in degrees * @param {number} params.argPerigee - Argument of perigee in degrees * @param {number} params.trueAnomaly0 - Initial true anomaly in degrees * @param {number} params.epoch - Initial epoch in milliseconds * @param {number} params.time - Propagation time in milliseconds * @returns {Object} Position in ECI coordinates (x, y, z in meters) */ propagate({ a, e = 0, i, raan, argPerigee, trueAnomaly0, epoch, time }) { const deg2rad = x => x * Math.PI / 180; const iRad = deg2rad(i); const Ω = deg2rad(raan); const ω = deg2rad(argPerigee); const ν0 = deg2rad(trueAnomaly0); const dt = (time - epoch) / 1000; // in seconds const n = Math.sqrt(this.mu / Math.pow(a, 3)); // rad/s const E0 = 2 * Math.atan(Math.tan0 / 2) * Math.sqrt((1 - e) / (1 + e))); const M0 = E0 - e * Math.sin(E0); const M = M0 + n * dt; function solveKepler(M, e, tol = 1e-12) { let E = M, delta = 1; while (Math.abs(delta) > tol) { delta = (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); E -= delta; } return E; } const E = solveKepler(M, e); const ν = 2 * Math.atan2( Math.sqrt(1 + e) * Math.sin(E / 2), Math.sqrt(1 - e) * Math.cos(E / 2) ); const r = a * (1 - e * Math.cos(E)); const x_p = r * Math.cos(ν); const y_p = r * Math.sin(ν); // Perifocal to ECI rotation const cosΩ = Math.cos(Ω), sinΩ = Math.sin(Ω); const cosω = Math.cos(ω), sinω = Math.sin(ω); const cosi = Math.cos(iRad), sini = Math.sin(iRad); const x = (cosΩ * cosω - sinΩ * sinω * cosi) * x_p + (-cosΩ * sinω - sinΩ * cosω * cosi) * y_p; const y = (sinΩ * cosω + cosΩ * sinω * cosi) * x_p + (-sinΩ * sinω + cosΩ * cosω * cosi) * y_p; const z = (sinω * sini) * x_p + (cosω * sini) * y_p; return { x, y, z }; } /** * Generates a CSV file with propagated positions * @param {Object} sat - Satellite parameters * @param {number} startTime - Start time in milliseconds * @param {number} endTime - End time in milliseconds * @param {number} interval - Time interval in milliseconds * @returns {string} CSV file content */ generateCSV(sat, startTime, endTime, interval) { let csvContent = "Time,X_km,Y_km,Z_km\n"; for (let time = startTime; time <= endTime; time += interval) { const position = this.propagate({ ...sat, time }); const date = new Date(time).toISOString(); // Convert from meters to kilometers const x_km = position.x / 1000; const y_km = position.y / 1000; const z_km = position.z / 1000; csvContent += `${date},${x_km.toFixed(9)},${y_km.toFixed(9)},${z_km.toFixed(9)}\n`; } return csvContent; } /** * Calcula el Greenwich Mean Sidereal Time (GMST) para una fecha dada (Date o ms) * @param {Date|number} date - Fecha en ms o Date * @returns {number} GMST en radianes */ gmst(date) { // Adaptado de satellite.js y Vallado const JD = (typeof date === 'number' ? date : date.getTime()) / 86400000 + 2440587.5; const T = (JD - 2451545.0) / 36525.0; let gmst = 280.46061837 + 360.98564736629 * (JD - 2451545.0) + 0.000387933 * T * T - (T * T * T) / 38710000.0; gmst = ((gmst % 360) + 360) % 360; // [0,360) return gmst * Math.PI / 180; // en radianes } /** * Convierte coordenadas ECI a ECEF * @param {Object} posECI - {x, y, z} en metros * @param {number} gmst - Greenwich Mean Sidereal Time en radianes * @returns {Object} {x, y, z} en metros (ECEF) */ eciToEcef(posECI, gmst) { const cosGmst = Math.cos(gmst); const sinGmst = Math.sin(gmst); return { x: posECI.x * cosGmst + posECI.y * sinGmst, y: -posECI.x * sinGmst + posECI.y * cosGmst, z: posECI.z }; } } // Export the class if (typeof module !== 'undefined' && module.exports) { module.exports = KeplerianPropagator; } else { window.KeplerianPropagator = KeplerianPropagator; }