
1. 为什么我的项目代号叫 Apollo-11先交代一下背景。我最近半年一直在搞一个航天任务仿真项目代号就叫 Apollo-11。很多人一听这个名字就以为是复刻登月历史其实不完全对。我做的是一个从发射窗口计算、轨道转移、着陆段仿真到上升对接返回全流程的任务推演系统之所以挂上 Apollo-11 这个代号是因为整个流程的骨架按照人类首次载人登月任务的公开技术路线来搭建。换句话说我不关心历史上谁说了哪句话、插了什么旗我只关心如果给我一套1969年的约束条件我能不能用现代工具把这条飞行路径重新算出来、仿真出来、可视化出来。这个项目本质上是三件事的合体第一把天文动力学里那些抽象公式变成能跑出具体数字的代码第二把一段经典飞行剖面拆解成可独立验证的模块每一个模块都能用公开的工程参数做校验第三用可视化把整个任务过程拉出来让不懂轨道力学的人也能看懂每一段飞行在干什么、为什么这么干。如果你是一个对航天、轨道力学、数值仿真或者游戏引擎里天体运动实现感兴趣的开发者这篇文章值得往下看。我不打算只给结论而是把我踩过的坑、推翻过的方案、最终的实现路径全部摊开。整个项目的代码量大概八千行左右核心用 Python 写轨道计算用三维可视化引擎做场景渲染数据库里存了一套公开的飞行时序参数。先说清楚项目范围这是我在动手前反复斟酌过的问题。如果真要做完整的 Apollo-11 模拟你得把火箭一级、二级、三级、登月舱、服务舱、指令舱的每一个子系统都建模那是一个动辄几十万行的工程。我给自己划的边界是不仿箭体内部结构不仿发动机热力学过程只仿真飞行轨迹层面的六个阶段——发射入轨、地月转移、月球制动、月面着陆、上升交会、返回再入。这六个阶段就是整个项目的骨架。每个阶段独立成一个计算模块模块之间通过状态向量传递数据比如入轨模块输出的是一个六要素轨道根数地月转移模块拿它作为初值继续算着陆模块拿到的则是高度、速度、燃料余量这些量。这种解耦方式让我能一个一个阶段攻克而不是一上来就被复杂系统压垮。2. 任务全流程拆解六个阶段的边界与衔接2.1 发射入轨段计时靠的是开普勒方程发射段的目标是把整个飞行器组合体送入一条近地停泊轨道。在真实任务里从点火到入轨大约用了 11 分钟左右入轨参数有一个非常明确的标称值近地点高度大约 190 公里远地点高度大约 190 公里轨道倾角大约 32.5 度偏心率接近 0。我在仿真里不打算模拟火箭发动机的推力曲线而是直接把入轨瞬间的轨道根数作为输入。这里有个关键点你从任何一个公开资料里拿到的入轨根数其实都是那一刻的瞬时根数但真实轨道是不断进动的因为地球不是标准球体J2 摄动会导致轨道面缓慢旋转。为了落地我引入了 J2 摄动项把升交点赤经的漂移率算出来确保后续的窗口计算在一个统一的参考系里。如果是纯二体运动开普勒方程可以用牛顿迭代或者布列特算法快速求解。我这套系统里把这两者都实现了因为用途不一样牛顿迭代适合单个点位的快速计算而布列特算法在长弧段递推时数值稳定性更好不会因为偏心率接近 1 而发散。2.2 地月转移段霍曼转移是基线实际要打偏置很多人以为地月转移就是标准的霍曼转移动量方向对准月球来时的速度方向就行。真不是。霍曼转移是共面圆轨道之间的最优双脉冲转移它假设目标在转移终点正好在轨道上等你。但月球一直在绕地球公转你不可能说我三点钟方向有个月球就打过去得算好它在转移终点时刻的精确位置和速度。我做这一阶段的方式是把问题拆成两个循环内循环使用二体模型搜索一串满足条件的转移轨道外循环修正发射时刻和入轨点位置让飞行器到达月球距离最近的时刻正好落在月球附近。说得更直白一点固定飞行时间约 70 小时然后扫描出发时刻观察每次到达时飞行器与月球中心的距离找出距离最小的一批窗口。这一步给我一个深刻的教训轨道计算里永远不要一开始就上全量精确模型先用简化的二体模型找到解的附近区域再逐步加修正项。否则你面对的是一个混沌的初值空间连收敛方向都找不到。2.3 月球制动与环月轨道宁可多烧燃料也要保住交会安全到达月球附近之后飞行器的地月转移轨道与月球轨道交汇相对速度约每秒 2.5 公里。如果不制动飞行器会直接擦过月球被甩出去。真实任务的策略是发动机反向点火把速度降到略高于月球逃逸速度的边界先进入一个大椭圆环月轨道然后再经过一次减速变成一条近圆轨道。我在这段里做了一件很实用的事把制动点火建模成一个有限推力弧段而不是简单的冲量变化。原因是冲量模型会把推进剂消耗算得过于乐观直接按理想速度增量换算燃料而实际情况中发动机推力越小、点火时间越长重力损失越大燃料消耗越高。真实任务的登月舱下降级发动机推力约 45 千牛上升级约 15.6 千牛服务舱主发动机约 97.6 千牛三个数字差异非常大如果都用冲量模型省事最后燃料账一定对不上。2.4 月面着陆段悬停、避障、选点这套逻辑至今不过时月面着陆是整个任务里最具戏剧性的一段从 15 公里高度开始动力下降速度要从大约每秒 1.7 公里降到 0高度要从 15 公里降到 0同时要保证着陆位置精度在百米量级。我在仿真里采用的是公开资料里经常提到的阿波罗制导方案本质是一种显式制导算法每过一小段时间根据当前位置和速度重新计算一条通往目标点的轨迹。这套方案最核心的指标是高度变化率与水平速度的比例关系。简单说如果你在高空就急着把水平速度消掉燃料会浪费在维持高度上如果你到低空再开始消速又会因为速度太大来不及修正就砸在月面上。制导律的关键是两个参数下降角控制律和刹车点高度。我花了很长时间调这两个数最终得到的着月速度为每秒 1 米左右和公开的真实数据能对上。这一阶段仿真还牵扯一个非常现实的问题地面并没有提前预知所有障碍物最后的避障是靠航天员目视修正完成的。我的程序里加了一个手动干预模拟通道在某个高度之后允许人为输入一个水平偏移量模拟航天员看到的实际月面状况。做这个设计的初衷是我想让整个仿真系统能像真实任务一样保留一条人参与决策的路径而不是一根筋走到底。2.5 上升与交会对接共面是硬约束时机是软约束上升段的目标是把两名航天员从月面送回环月轨道的指令舱并在轨道上交会对接。这个阶段我最开始做得非常痛苦因为没有意识到共面是最硬的约束。所谓共面就是上升器的起飞时机必须选在指令舱轨道面扫过着陆点上空的那一刻附近起飞。原因很简单轨道面由发射时刻的速度矢量决定而两个航天器要在同一轨道上交会它们的轨道面必须重合。轨道面的分离速度非常昂贵因为要额外付出一大笔速度增量来改变轨道倾角。所以真实任务的上升时间是严格卡在某个窗口内的而不是随便什么时候点上升按钮就能走。在仿真里我把这个约束处理成两层逻辑第一层先找出上升器起飞后到达预定交会点的时间窗口第二层反推起飞时刻使得起飞后上升器所处的轨道面与指令舱轨道面的夹角小于某个容差。最后还要处理交会段的相位对齐也就是说不仅轨道面要重合两个人到达交会点的时间也要一致后者是软约束可以通过微调飞行时间来解决。2.6 返回再入段用两次气动减速换一次安全落地从环月轨道回家的最后一步也要经历两次大气层再入。第一次再入会进入大气层很浅的位置利用气动阻力把速度从每秒 11 公里降到大约每秒 7.8 公里随后飞行器重新跃出大气层进入一段滑翔弧第二次再入才真正降落到海面。我在这段里使用的模型是一个简化升力体模型只计算弹道系数和升阻比两个参数。关键调试点是再入角。再入角过浅飞行器会被大气层弹出去无法充分减速再入角过深过载会高到人体无法承受。真实任务再入通道的宽度只有几十公里容错窗口非常窄。我花在参数扫描上的时间比写代码还多最后确定了一个进入角约 6.5 度、峰值过载约 6.8 个重力加速度的剖面和公开数据基本一致。3. 轨道计算模块的实现从二体模型到数值递推的取舍3.1 为什么二体模型永远是第一版的首选新建任何一个轨道计算模块我的建议永远是先做二体模型把它跑通、跑准再往上面叠摄动项。二体模型的定义是只有两个天体参与引力相互作用不考虑第三个天体也不考虑非球形引力场。优点是解有解析形式任意给定时间都能直接算出位置和速度不需要数值积分。在这个项目里地月转移段和环月段的初版都用了二体假设地球到月球的问题简化为飞行器在地球引力场中的受控运动月球被当成移动的引力源而不是一个引力源本身。你会问那还怎么能算得准答案是在这个阶段二体解的主要作用不是算准而是把整个任务剖面框出来。你大概知道要走 70 小时、大概知道入轨点在哪、大概知道到达时月球在哪这就是极大的工程价值。真正的高精度数值计算我是在这一段验证完后再切入的。切入方式是引入高精度积分器比如 RKF7(8) 或者 DOP853这两种都是变步长的龙格-库塔法的扩展版本能根据误差自动调节步长。对业余项目来说RKF7(8) 足够DOP853 更稳但稍慢我最终选了 DOP853 作为全链路递推的基准积分器。3.2 坐标系的统一是本项目最容易翻车的部分先给一个数据我在做轨道计算时先后踩过坐标系不一致的坑直接导致计算结果偏差了 700 公里查了整整两天才定位到问题。原因是程序中同时存在三个坐标系——地心惯性坐标系、地心固连坐标系、月心坐标系不同模块各有偏好但我没有在数据入口做统一转换。解决办法是在整个项目里强制使用一套内部基准坐标地心惯性坐标系J2000 历元所有外部数据进入系统时先转换成这个坐标系所有计算结果也统一输出成这个坐标系。只有在最后可视化阶段才把地心惯性坐标转换到适合展示的月心或地面视角。程序员之间常开玩笑说搞天文仿真的人一半时间在算物理另一半时间在找坐标系 bug这句话一点不夸张。3.3 递推步长怎么选既要精度又要速度数值轨道递推的步长选择是个矛盾点。步长太小计算量巨大整个仿真可能跑几分钟才能出一秒的结果步长太大误差指数级累积甚至会让飞行器飞穿月球表面。我用 DOP853 的时候实际是让它自适应步长的但设了一个粗步长上限 60 秒细步长下限 0.1 秒。关键判断标准是当飞行器接近月球或地球时引力梯度变化剧烈需要更小步长在远离大天体时步长可以放宽。自适应实质上是让积分器自己感知这个变化而不是靠人手工分段设步长。项目里我还做了一次对比测试固定步长为 10 秒跑完全段因为中途经过拉格朗日点附近的敏感区域最终位置误差比自适应模式大了一个数量级。4. 着陆、上升与交会三个模块的制导逻辑是怎么联动的4.1 着陆段的显式制导是怎么把高度、速度、燃料绑在一起的我在前面提到着陆段用了显式制导具体公式可以理解为飞行器每 0.1 秒重新计算一次当前状态和目标状态之间的误差然后按照误差反推需要的加速度方向与大小。这个误差不是简单地高度差了 3000 米、速度差了每秒 500 米这么机械而是定义成一个多项式函数把高度、高度变化率、水平位置、水平速度四个量映射成一个标量误差。我调试中发现不能让这个映射关系太激进否则飞行器会为了快速消除水平速度而垂直往下掉或者为了控制高度而水平飞出去。最终我加入了一个权重曲线早期阶段的权重放在水平方向的速度消除上后期阶段的权重转向垂直方向。这个过程像极了新手学开车——教练会告诉你先保证方向再慢慢降速等到靠近车位的时候你其实已经没有多少水平速度需要处理了。着陆模块的燃料账也很能说明问题。我把下降级燃料总量设在约 8200 公斤推进剂消耗率按发动机质量流量来算。我用程序跑完整个动力下降阶段得到的剩余燃料约为 350 公斤和公开资料里的剩余 400 公斤左右量级一致说明制导律的调优方向是对的。4.2 上升段的起飞窗口到底是怎么算出来的上升段的起飞窗口本质上是轨道面重合窗口和交会相位的复合解。计算过程分三步第一步用指令舱当前轨道根数算出轨道面法向量第二步计算着陆点随月球自转扫过这个轨道面的时刻第三步在这些时刻中再筛选出上升器飞行到交会点时指令舱正好到达的时间组合。这里有个容易忽略的坑着陆点其实也是月面上一个移动的点因为月球在自转。这意味着起飞时刻必须考虑月面位置相对轨道面的相位变化。真实任务中上升器从月面起飞后会进入一条低椭圆轨道然后通过数次变轨抬升远月点高度最终与指令舱在一条大约 100 公里高的圆轨道上交会。我这套代码里也实现了多脉冲变轨策略第一个脉冲在近月点把远月点抬到与目标轨道高度一致第二个脉冲在远月点把近月点抬到同样高度完成轨道圆化。如果你仿真到这一步会特别真切地体会到上升交会其实是最需要算计的环节每一个脉冲的施加时间和大小都像解一道递进方程前一步错了后一步无论如何也追不回来。4.3 交会段的相对运动从绝对轨道到相对椭圆的视角切换很多做交会仿真的新手都会栽在相对运动这个概念上。两个飞行器都在各自轨道上飞看上去都在动怎么判断彼此距离和方位实际上标准做法是切换到以目标飞行器为原点的相对坐标系。在这个坐标系里追踪飞行器的相对轨迹会呈现一个前后来回摆动的椭圆这也是交会制导中最经典的霍曼交会图示。我做的交会算法并不复杂先用绝对轨道递推算出两个飞行器每一个时刻的位置再转换到目标飞行器的轨道坐标系里计算相对位置矢量。然后设定一个交会半径门限比如 100 米当两个飞行器距离小于这个门限时输出交会完成。为了让画面好看我还把相对运动轨迹画了出来效果非常直观那种螺旋接近曲线让第一次看的同事都以为我在放什么科幻电影的素材。5. 实测数据盘点误差从哪来、怎么修、修完还剩多少5.1 我保存的每一次运行结果都是排查问题的钥匙这个项目我跑了不下一百个整段任务仿真每一次运行都会把完整的状态历史保存成结构化数据文件。为什么要保留每一次的结果因为轨道计算里的问题经常是间歇性的你以为修好了一个参数跑两次又出错如果没有存档只凭记忆排查很可能把真正的问题掩盖掉。项目里我遇到过最典型的一次问题着陆模块在约 80% 的任务场景下都能正常工作但剩余 20% 场景会出现燃料提前耗尽。后来我对比了十几份运行记录发现这些问题场景的共同点是着陆起始高度比标称值高了大约 3%。原因是前一个模块传递给着陆模块的初值里带着微小误差着陆制导在应对高空起始点时触发了某种振荡。最终我在着陆模块入口加了一个状态平滑滤波器把初值的高频抖动滤掉问题才稳定消除。5.2 月球引力场模型从球谐展开换到简化椭球我在刚开始做环月轨道递推时使用的是最简单的均匀球体引力场。问题很快暴露在对环月轨道做长弧段递推时轨道近月点高度会出现几十公里的漂移这根本不是真实情况。后来我引入了月球 J2 摄动项数值上相当于把月球从完美球体改成旋转椭球体。这一个改动就让轨道预报的精度大幅提升。更精细的引力场模型包含数十个球谐系数但业余项目没必要一开始就用那么复杂的东西。我的顺序建议是先均匀球再加 J2再根据误差需求决定要不要引入更高阶摄动。5.3 把仿真输出与公开任务参数做比较验证没有验证的仿真只是自嗨。我做了两层验证外层验证是把任务各阶段的关键时刻节点与公开的历史飞行时序对比比如入轨时刻、到达月球时刻、着陆时刻、返回入再时刻误差控制在 5 分钟以内内层验证是把每个阶段的终点状态与公开的速度、高度数值对比。下表是我在项目最终版本上跑出来的一组对比数据左侧是程序输出右侧是公开资料中的标称值。阶段参数仿真输出值公开标称值偏差入轨高度近地点189.2 公里约 190 公里1%地月转移飞行时间71.4 小时约 71 小时1%到达月球时相对速度2.44 公里/秒约 2.5 公里/秒3%着陆后剩余下降级燃料342 公斤约 400 公斤15%返回舱再入峰值过载6.8 g约 6.7 g2%着陆剩余燃料偏差 15% 是我唯一不太满意的数据点。原因大概率是我对下降级质量流量的建模偏保守把发动机比冲设低了一点进一步的分析需要更精细的发动机性能数据但作为项目验证来说这个量级的偏差已经足以证明整套制导逻辑和轨道模型是自洽的。6. 从 Apollo-11 项目里带走的三件东西这个项目做完之后我最值钱的收获其实不是那几千行代码而是三条对任何仿真类项目都通用的经验。第一不要迷信高精度模型。数值仿真的正确路线是从简单模型起步用简单模型理解问题的结构再逐步叠加复杂因素。我一开始就想用完整的月球重力场模型结果连续两周数据乱飞反而在退回二体模型之后才把任务剖面捋顺。复杂模型是工具不是目的。第二用误差对比驱动开发而不是用感觉驱动开发。每次改动都要有基线数据作为对照。你的仿真输出跟标称值差了 3%这个 3% 是从哪来的比我觉得这次跑得很合理有价值得多。给每一个模块设置一个验收门槛过了门槛再往下走不过就停下来排查。第三把整套系统的数据流画明白。我知道有人嫌这个建议太基础但在我实测过程中90% 以上的问题都出在模块间数据接口上比如坐标系混用、初值污染、时序错位。只要把上一个模块输出什么格式的数据、下一个模块期望什么格式的数据这个契约定义清楚整个项目几乎就能按部就班推到底。最后再分享一个小细节。我的项目目录至今保留着一个叫 run_0720 的目录里面存的是第一次全链路跑通、六个阶段全部衔接成功的那次运行日志。那次运行之后我开始给代码的每一个函数写用途注释因为我知道这个项目已经不只是玩玩而已它变成了一套真正能把一个历史经典任务完整推演出来的系统。如果你也对这类仿真感兴趣建议你从一个阶段开始比如只做环月轨道递推跑通了再往前后延展你会在每一个阶段的突破里获得完全的掌控感而这比代码本身更让人上瘾。