本文介绍如何基于Skyfield&Orekit中SGP4轨道位置计算并简单讨论其在卫星网络仿真中应用。
SGP4
TLE(Two-line element set)轨道根数对应的计算模型是简化普适模型 (simplified perturbations models,包括SGP, SGP4, SDP4, SGP8 and SDP8),SGP(Simplified General Perturbations)简化摄动模型适用轨道周期小于225分钟的近地物体,SDP(Simplified Deep Space Perturbations)简化深空摄动模型适用于轨道周期大于 225 分钟的物体。
SGP4 模型星历误差大约1km并且每天增长大约1–3km误差。最初的 SGP 模型由 Kozai 于 1959 年开发,由 Hilton & Kuhlman 于 1966 年完善,最初由National Space Surveillance Control Center(以及后来的United States Space Surveillance Network ))用于跟踪轨道上的物体,SDP4模型在历元上的误差为10公里。
SGP4模型具体算法请自行研究,本人不做天文研究仅利用该模型做位置预测,卫星网络仿真可能能应用上。
Skyfield&Orekit
Skyfield 是一个优雅的天文学Python软件包,Skyfield 可用于计算恒星、行星、 以及绕地球轨道运行的卫星。 Skyfield中提供了SGP4模型相关API,基于相关api可用来预测卫星位置。Skyfield根据MIT License开源分发。
Orekit )是一个用 Java 编写的低级空间动力学库,自 2008 年以开源许可证发布以来,获得了广泛的认可。Orekit 旨在为飞行动力学应用的开发提供准确、高效的低级组件。Orekit 根据Apache License version 2.0开源分发。另外社区维护了一个 Python 包装器orekit-python-wrapper ,可基于Python使用Orekit API。
坐标系
对卫星进行轨道位置计算需要使用到如下天文相关坐标系统:
ECI(Earth-Centered, Inertial/地心惯性坐标系)坐标系统,ECEF(Earth-centered, Earth-fixed/地心地固坐标系)坐标系统及WGS84经纬度。
ECEF与ECI坐标系有着相同的坐标原点和z轴定义,但ECEF坐标系是与地球保持同步旋转。
ITRS(International Terrestrial Reference System/国际地球参考系统)是一种ECEF,不受地球自转、地球赤道扭曲等影响,因此可以更精确地反映地球表面的物理位置。
ITRF(International Terrestrial Reference Frame/国际地球参考框架是ITRS的一个实现。
地表物理位置唯一的WGS84经纬度坐标可转化为唯一的ITRS坐标,通常可用于定位卫星地面站。
PS:具体坐标系相关详细标准请自行查阅。
Skyfield SGP4轨道位置计算
如下以STARLINK-1007卫星为例,计算当前卫星ECI坐标,ECEF坐标及WGS84经纬度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import time from sgp4.api import Satrec, WGS84 from skyfield.api import EarthSatellitefrom skyfield.api import load as skyfield_load from skyfield.api import wgs84from skyfield import framelibts = skyfield_load.timescale(builtin=True ) class Satellite (): id = 0 name = "" tle_line1 = "" tle_line2 = "" sat = None sw_name = "" sw_dpid = "" def __init__ (self, id , line0, line1, line2 ): self.id = id self.name = line0 self.tle_line1 = line1 self.tle_line2 = line2 satrec = Satrec.twoline2rv(line1, line2, WGS84) self.sat = EarthSatellite.from_satrec(satrec, ts) print (self.sat) def frame_now (self ): sat_obj = self.sat.at(ts.now()) position = sat_obj.position.km velocity = sat_obj.velocity.km_per_s ecef = sat_obj.frame_xyz_and_velocity(framelib.itrs) lat, lon = wgs84.latlon_of(sat_obj) alt = wgs84.height_of(sat_obj).km return {'position_eci_x' : position[0 ], 'position_eci_y' : position[1 ], 'position_eci_z' : position[2 ], 'velocity_eci_x' : velocity[0 ], 'velocity_eci_y' : velocity[1 ], 'velocity_eci_z' : velocity[2 ], 'position_ecef_x' : ecef[0 ].km[0 ], 'position_ecef_y' : ecef[0 ].km[1 ], 'position_ecef_z' : ecef[0 ].km[2 ], 'velocity_ecef_x' : ecef[1 ].km_per_s[0 ], 'velocity_ecef_y' : ecef[1 ].km_per_s[1 ], 'velocity_ecef_z' : ecef[1 ].km_per_s[2 ], 'geod_latitude' : lat.degrees, 'geod_longitude' : lon.degrees, 'geod_altitude' : alt} if __name__ == "__main__" : line0 = "STARLINK-1007" line1 = "1 44713U 19074A 24120.93093052 .00013004 00000+0 89012-3 0 9990" line2 = "2 44713 53.0547 353.6824 0001286 89.2500 270.8636 15.06406908246617" sat = Satellite(0 , line0, line1, line2) for i in range (0 , 10 ): frame_data = sat.frame_now() x = frame_data['position_ecef_x' ] y = frame_data['position_ecef_y' ] z = frame_data['position_ecef_z' ] vx = frame_data['velocity_ecef_x' ] vy = frame_data['velocity_ecef_y' ] vz = frame_data['velocity_ecef_z' ] lon = frame_data['geod_longitude' ] lat = frame_data['geod_latitude' ] alt = frame_data['geod_altitude' ] timestamp = time.strftime("%Y-%m-%d %H:%M:%S" , time.localtime(time.time())) print ("tm" , timestamp, "\n\tx:" , x, "y:" , y, "z:" , z, "\n\tvx:" , vx, "vy:" , vy, "vz:" , vz, "\n\tlon:" , lon, "lat:" , lat, "alt:" , alt) time.sleep(10 )
计算结果与n2yo.com对比:
Orekit轨道位置计算
同样以STARLINK-1007卫星为例,计算当前卫星ECEF坐标(ITRS)及WGS84经纬度,基于orekit 11.2和hipparchus 3.0版本。
Java Demo
maven pom依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <dependencies > <dependency > <groupId > org.orekit</groupId > <artifactId > orekit</artifactId > <version > 11.2</version > </dependency > <dependency > <groupId > org.hipparchus</groupId > <artifactId > hipparchus-core</artifactId > <version > 3.0</version > </dependency > <dependency > <groupId > org.hipparchus</groupId > <artifactId > hipparchus-fitting</artifactId > <version > 3.0</version > </dependency > <dependency > <groupId > org.hipparchus</groupId > <artifactId > hipparchus-optim</artifactId > <version > 3.0</version > </dependency > <dependency > <groupId > org.hipparchus</groupId > <artifactId > hipparchus-ode</artifactId > <version > 3.0</version > </dependency > <dependency > <groupId > org.hipparchus</groupId > <artifactId > hipparchus-geometry</artifactId > <version > 3.0</version > </dependency > </dependencies >
java SGP4计算demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import org.hipparchus.geometry.euclidean.threed.Vector3D; import org.hipparchus.util.FastMath; import java.io.File; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.orekit.bodies.GeodeticPoint; import org.orekit.bodies.OneAxisEllipsoid; import org.orekit.data.DataContext; import org.orekit.data.DataProvidersManager; import org.orekit.data.DirectoryCrawler; import org.orekit.errors.OrekitException; import org.orekit.frames.Frame; import org.orekit.frames.FramesFactory; import org.orekit.propagation.SpacecraftState; import org.orekit.propagation.analytical.tle.TLE; import org.orekit.propagation.analytical.tle.TLEPropagator; import org.orekit.time.AbsoluteDate; import org.orekit.time.TimeScalesFactory; import org.orekit.utils.Constants; import org.orekit.utils.IERSConventions; import org.orekit.utils.PVCoordinates; public class OrekitSgp4Demo { public static final String OREKIT_PATH_NAME = "orekit-data" ; private static void initOreKit () throws OrekitException { String resPath = OrekitSgp4Demo.class.getClassLoader().getResource("" ).getPath(); String orekitPhysicalDataLocation = resPath + "/" + OREKIT_PATH_NAME; System.out.println("orekit path: " + orekitPhysicalDataLocation); File orekitData = new File (orekitPhysicalDataLocation); if (!orekitData.exists()) { throw new RuntimeException ( "Unrecoverable error: Could not locate orekit physical data at default location of '" + orekitPhysicalDataLocation + "'!" ); } DataProvidersManager manager = DataContext.getDefault().getDataProvidersManager(); manager.addProvider(new DirectoryCrawler (orekitData)); } public static TLE buildTLEFromLines (String tleLine1, String tleLine2) throws OrekitException { if (!TLE.isFormatOK(tleLine1, tleLine2)) { throw new RuntimeException ("Bad TLE format!\n" + tleLine1 + '\n' + tleLine2 + '\n' ); } return new TLE (tleLine1, tleLine2); } public static String tle1Line0 = "STARLINK-1007" ; public static String tle1Line1 = "1 44713U 19074A 24120.93093052 .00013004 00000+0 89012-3 0 9990" ; public static String tle1Line2 = "2 44713 53.0547 353.6824 0001286 89.2500 270.8636 15.06406908246617" ; public static void main (String[] args) throws InterruptedException { initOreKit(); TLE tle = buildTLEFromLines(tle1Line1, tle1Line2); TLEPropagator tleProp = TLEPropagator.selectExtrapolator(tle); Frame itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, true ); OneAxisEllipsoid oae = new OneAxisEllipsoid ( Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf); for (int i = 0 ; i < 10 ; i++) { Date now = Calendar.getInstance(TimeZone.getTimeZone("GMT+:08:00" )).getTime(); AbsoluteDate absoluteDate = new AbsoluteDate (now, TimeScalesFactory.getUTC()); SpacecraftState state = tleProp.propagate(absoluteDate); PVCoordinates pvInITRF = state.getPVCoordinates(itrf); Vector3D position = pvInITRF.getPosition(); Vector3D velocity = pvInITRF.getVelocity(); GeodeticPoint gp = oae.transform(position, itrf, absoluteDate); double longitude = FastMath.toDegrees(gp.getLongitude()); double latitude = FastMath.toDegrees(gp.getLatitude()); double altitude = gp.getAltitude(); System.out.println( "time:" + now + " \n\tx:" + position.getX() + "m, y:" + position.getY() + "m, z:" + position.getZ() + "m, \n\tvx:" + velocity.getX() + "m/s, vy:" + velocity.getY() + "m/s, vz:" + velocity.getZ() + "m/s, \n\tlon:" + longitude + "°, lat:" + latitude + "°, alt:" + altitude + "m" ); TimeUnit.SECONDS.sleep(10 ); } } }
计算结果与n2yo.com对比:
Python Demo
基于Python调用Orekit最好使用conda安装环境,如下:
1 2 3 4 conda create -y -n orekit_env -c conda-forge orekit=11.2 conda activate orekit_env pip install numpy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import orekitfrom orekit.pyhelpers import setup_orekit_curdirfrom org.orekit.frames import FramesFactory, TopocentricFramefrom org.orekit.bodies import OneAxisEllipsoid, GeodeticPointfrom org.orekit.utils import IERSConventions, Constantsimport timeimport numpy as npfrom datetime import datetime, timezonefrom org.orekit.propagation.analytical.tle import TLE, TLEPropagatorfrom org.orekit.time import AbsoluteDate, TimeScalesFactoryvm = orekit.initVM() setup_orekit_curdir("./orekit-data.zip" ) itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, True ) earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf) if __name__ == "__main__" : line0 = "STARLINK-1007" line1 = "1 44713U 19074A 24120.93093052 .00013004 00000+0 89012-3 0 9990" line2 = "2 44713 53.0547 353.6824 0001286 89.2500 270.8636 15.06406908246617" tle = TLE(line1,line2) print (tle) print ('Epoch :' , tle.getDate()) propagator = TLEPropagator.selectExtrapolator(tle) for i in range (0 , 10 ): date = datetime.utcnow().replace(tzinfo=timezone.utc) now = date.astimezone() seconds = float (date.second) + float (date.microsecond) / 1e6 absoluteDate = AbsoluteDate(date.year, date.month, date.day, date.hour, date.minute, seconds, TimeScalesFactory.getUTC()) state = propagator.propagate(absoluteDate) pv = state.getPVCoordinates(itrf) p = pv.getPosition() v = pv.getVelocity() gp = earth.transform(p, itrf, absoluteDate) print ("tm" , now, "\n\tx:" , p.getX(), "y:" , p.getY(), "z:" , p.getZ(), "\n\tvx:" , v.getX(), "vy:" , v.getY(), "vz:" , v.getZ(), "\n\tlon:" , np.rad2deg(gp.getLongitude()), "lat:" , np.rad2deg(gp.getLatitude()), "alt:" , gp.getAltitude()) time.sleep(10 )
计算结果与n2yo.com对比:
卫星网络仿真应用
卫星网络如Starlink星座,卫星移动模型可以基于sgp4模型(Skyfield/orekit/…)进行位置预测,坐标系上自然需要使用考虑不受地球自传影响的ITRS坐标系,地面站通常需要WGS84经纬度转换为ITRS坐标系下固定坐标(相当于地面站移动模型是固定不动的)。
WGS84转ITRS坐标系可参考wgs84-geocentric 进行转换,如下为北京地区转换:
卫星网络中卫星及地面站通过ITRS坐标系建立3D网络拓扑图,根据连接关系和位置距离可初步对星地链路建模(如波速分配覆盖/连接可用性/带宽/延迟等)和星间链路建模(如带宽/延迟/丢包率等),根据全局动态拓扑时间切片后拓扑也可该时间切片下最短路径等计算,并根据路径配置路由进行网络仿真。