System 与 Dynamics:两棵类层次的数据流

e2m2e/algorithm/dynamics/ 下有两棵类层次:System 一侧描述"这是什么系统" (μ、特征尺度、平动点、星历、单位、坐标系),Dynamics 一侧描述"怎么把它积分 出来"(积分器、容差、STM、事件、结果缓存)。包 docstring 概括为"System(数据 上下文)+ Dynamics(传播编排)"(e2m2e/algorithm/dynamics/__init__.py:1)。 本文按数据流动的方向,把这两个家族各自是什么、谁读它们的什么成员、一次传播里 数据走哪条路,逐一讲清楚。文中所有行号以当前代码为准。

从一次传播说起

先看最常见的一个场景:传播一条地月 CR3BP 轨迹。测试套件里它的标准形态是 (tests/conftest.py:13-25):

system = CR3BP_System(mu=Datum.DE421.mu, primary="Earth", secondary="Moon")._with_default_scales()
dynamics = CR3BP_Dynamics(system=system)
result = dynamics.propagate(state0, (0.0, 6.3))

数据在这条链上走四段路。

第一段,构造 System。 CR3BP_System.__init__ 只收质量参数 μ、两个天体名和 可选的天体半径,特征长度、特征时间、五个平动点全部置为 None (e2m2e/algorithm/dynamics/cr3bp_system.py:71-128)。此时的系统还不能参与计算: DU/TU/VU 属性在尺度未设时抛"系统未初始化" (cr3bp_system.py:194-212)。_with_default_scales() 按天体对补上特征尺度 (cr3bp_system.py:130-160),平动点则推迟到首次需要时才解算: get_libration_point 发现没算过就先调 compute_libration_points (cr3bp_system.py:293-317)。System 的构造因而是两段式的:先定"哪个系统", 再定"用什么尺度量化它"。

第二段,构造 Dynamics。 CR3BP_Dynamics(system) 做的事很轻:存下 system 引用,填一套默认积分器配置(RK45、rtol/atol 1e-12、max_step 0.01),把结果缓存 置空(e2m2e/algorithm/dynamics/dynamics.py:78-97、544-553)。它不复制系统的任何 参数。μ 在每次算加速度时经 self.system.mu 现取(dynamics.py:593)。

第三段,propagate。 无事件时 CR3BP 走 Rust 快速路径,Python 侧只把 mu、 时间区间、初值和积分配置这组标量传过 FFI(dynamics.py:851-859);轨迹在 Rust 侧算完,以 {"time", "states"} 字典回来,同时写进 self.last_trajectory 缓存 (dynamics.py:861-870)。这一段的细节见后文"propagate 内部"一节。

第四段,结果进数据容器。 设计链路把返回字典装进 Orbit,并把 system 引用 一并塞进去:Orbit(states=result["states"], times=result["time"], system=dynamics.system)(e2m2e/algorithm/design/design_orbit.py:550)。从此这组 浮点数带着自己的单位与坐标系解释者旅行;之后谁想再做计算,可以从 orbit.system 重新长出一个 dynamics,StabilityAnalysis 正是这样按需重建的 (e2m2e/algorithm/stability.py:95-98)。

四段路合起来就是两个家族的分工:System 是长期持有的模型上下文,Dynamics 是 围着它转的、带配置与缓存的传播者。

System:模型上下文

基类只承诺三个成员

System ABC 定义的最小接口只有三项:frame(坐标框架)、unit_system (单位系统)、gravitational_parameter(body)(引力参数) (e2m2e/algorithm/dynamics/system.py:15-51)。docstring 点名了什么刻意不进基类: mubody_state(body, t)coordinate_system 属于特定实现的概念 (system.py:25-27)。基类另有一个非抽象的 get_body_position,默认抛 NotImplementedError:星历专属能力,放在基类只是给一个明确的报错位置 (system.py:54-66)。

接口薄是有后果的:想对两种系统多态的代码,只能依赖这三个成员,其余都得用 getattr/hasattr 现探。下文"消费面"一节会看到这是真实发生的模式。

CR3BP_System:无量纲、自治、两段式初始化

构造注入五样东西:mu、主/次天体名、可选的主/次天体半径 (cr3bp_system.py:71-80)。构造时做两道校验:半径若给必须为正 (cr3bp_system.py:93-98),μ 必须落在 (0, 0.5)(cr3bp_system.py:102-106)。 构造完成后还有两步初始化可选:set_characteristic_scales(distance, period) 由 距离与周期推出特征长度/时间/速度并置 is_initialized(cr3bp_system.py:214-235); compute_libration_points()fsolve 解三个共线点、解析给出两个三角点 (cr3bp_system.py:237-291,其中 264-266 数值解、268-274 解析解)。

对基类三成员,它的回答是:会合(旋转)坐标系、无量纲单位、约定总质量为 1 时 primary 的 GM 是 1 - mu、secondary 的是 mu(cr3bp_system.py:163-191)。

初始化之后,这个对象向外提供四类数据:

  • 特征尺度:DU(km)、TU(天)、VU(m/s)三个属性,外加 characteristic_length/time/velocity 原始字段(cr3bp_system.py:108-110、194-212)。

  • 平动点:L1L5L_points 字典(cr3bp_system.py:112-117)。

  • Jacobi 常数:get_jacobi_constant(state),Parker 约定 (cr3bp_system.py:319-354)。

  • 单位换算与稳定性:dimensionless_to_physical / physical_to_dimensionless (cr3bp_system.py:356-396)、compute_stability_index(cr3bp_system.py:398-442)。

半径字段(primary_radius_km/secondary_radius_km)本身不参与动力学,是碰撞 检测的数据来源,流向见"事件与碰撞"小节。

EphemerisSystem:SPICE 查询的统一入口

构造注入五样东西:天体名列表、已完成内核加载的 SPICEManager、参考原点 (默认 "EARTH")、坐标框架(默认 J2000)、可选的 CoordinateSystem (e2m2e/algorithm/dynamics/ephemeris_system.py:35-55)。对基类三成员,它的回答 是:构造时给定的框架、物理单位、GM 直通 spice.get_gm (ephemeris_system.py:59-103)。

与 CR3BP 的两段式初始化不同,这里的"第二步"是给 coordinate_system 赋值,且 发生在构造之后、经 property setter 完成(ephemeris_system.py:69-75)。编排层 的标准写法是先构造再补:system = EphemerisSystem(...) 接着 system.coordinate_system = CoordinateSystem(...) (e2m2e/algorithm/propagation.py:118-123)。力模型传播依赖这个字段,不设会被 ForceModel 拒绝(e2m2e/algorithm/forces/force_model.py:57-58)。

星历数据经四个查询方法流出:gravitational_parameter/get_gm(单体 GM)、 get_gm_values(按 bodies 顺序的 GM 数组)、get_body_position(位置)、 get_body_state(六维状态)(ephemeris_system.py:94-150)。另有一个 update_coordinate_systems(t, state) 用于推进动态坐标系 (ephemeris_system.py:77-92);ForceModel 传播下沉 Rust 后,逐步更新由 Rust 内部完成,e2m2e/ 内已无调用点,仅测试保留 (tests/numerical/forces/container/test_force_model_dynamic_axes.py:143-186)。

BCR4BPSystem:继承 CR3BP_System,叠加太阳

BCR4BPSystem 继承 CR3BP_System (e2m2e/algorithm/dynamics/bcr4bp_system.py:23),构造时多收四个太阳参数: sun_masssun_distancesun_angular_ratesun_phase0;前两个缺省时按 DE440 常量和日地平均距离推导(bcr4bp_system.py:50-106,缺省推导在 89-92)。 sun_angular_rate 特殊:它依赖特征时间,直接构造不给时暂存 None,由 set_characteristic_scales 覆写方法按儒略年公转推导(bcr4bp_system.py:148-161); 因此标准入口是类方法 BCR4BPSystem.earth_moon(),一步完成构造与尺度设置 (bcr4bp_system.py:109-132)。

太阳位置不查星历,是时间 t 的解析函数 sun_position(t),即会合系里的共面圆周 (bcr4bp_system.py:163-183)。gravitational_parameter 在 "primary"/"secondary" 之外多接受 "sun"(bcr4bp_system.py:185-193)。注意 docstring 的提醒:BCR4BP 无 Jacobi 积分,compute_libration_points 给出的是对应 CR3BP 的平动点,仅作参考 位置(bcr4bp_system.py:41-42)。

Dynamics:传播编排者

基类持有的三类状态

Dynamics.__init__ 只收一个 system(dynamics.py:78-84),随后在实例上放三类 东西:

  1. system 引用self.system,传播全程只读不写(dynamics.py:84)。

  2. 积分器配置integratorrtolatolmax_step,默认值挂在类常量上 (dynamics.py:86-91)。这些是公开字段,调用方构造后直接改:并行搜索按任务 改容差与步长(e2m2e/algorithm/transfer/search_parallel.py:837-842),多重打靶 构造后立刻覆写三项(e2m2e/algorithm/solver/multiple_shooting.py:97-101),测试 fixture 为提速放宽星历传播的容差(tests/conftest.py:92-100)。

  3. 结果缓存last_trajectorylast_stm,每次传播结束时覆写 (dynamics.py:93-94;写入点见后文)。CR3BP_Dynamics 再加 jacobi_history/jacobi_error 两个 Jacobi 监测缓存(dynamics.py:552-553)。

也就是说,Dynamics 是有态工人:配置与最近一次的结果都留在实例上,供调用方 事后取(如 compute_state_transition_matrix 内部就是一次传播后取末态 STM, dynamics.py:986-999)。

模板方法:propagate 是骨架,子类填钩子

propagate() 是模板方法:基类定算法骨架(参数规范化 → 校验 → 分发 → 结果 装配),子类经两组钩子参与:_get_eom_func(with_stm) 给出 ODE 右端, _get_max_step(t_span) 给步长上限(dynamics.py:100-125)。分发只有两支: _propagate_with_stm(42 维增广状态)与 _propagate_state_only(6 维) (dynamics.py:223-229)。基类的两支实现都走 scipy solve_ivp;三个子类各把 这两支覆写为 Rust 快速路径优先(事件场景的处理因子类而异,见后文)。

三个子类,两种继承选择

CR3BP_Dynamics(dynamics.py:522)与 EphemerisDynamics (e2m2e/algorithm/dynamics/ephemeris_dynamics.py:47)分别实现自治无量纲 CR3BP 方程与含时物理单位 N 体方程。BCR4BP_Dynamics 直接继承 Dynamics (e2m2e/algorithm/dynamics/bcr4bp_dynamics.py:42),不继承 CR3BP_Dynamics, 尽管它的方程就是 CR3BP 加一项太阳摄动。代码里能看到这个选择的理由:

  • 雅可比签名不同:CR3BP 自治,compute_jacobian_A(state)(dynamics.py:611); BCR4BP 含时,compute_jacobian_A(t, state)(bcr4bp_dynamics.py:123)。

  • Jacobi 语义反转:CR3BP_Dynamics 构造即带 Jacobi 监测缓存 (dynamics.py:552-553),而 BCR4BP 是时间周期系统、无 Jacobi 积分,必须把 compute_jacobi_constant_handle_jacobi 都实现为抛 NotImplementedError (bcr4bp_dynamics.py:457-463),继承来的能力要逐个点掉。

  • STM 入口多一个参数:含时系统的 Φ 依赖起止时刻, compute_state_transition_matrix 多一个 t0(bcr4bp_dynamics.py:438-455)。

  • Rust 入口不同:BCR4BP 的传播函数要多带四个太阳参数 (bcr4bp_dynamics.py:293-303)。

继承 CR3BP_Dynamics 意味着几乎每个公开方法都要覆写、还要压掉 Jacobi 机制。 仓库里有过同型教训:ForceModel 一度形式上继承 Dynamics 只为复用几个数据属性, 实则全部重写 propagate 并对 STM/Jacobi 抛错,被认定为 LSP 违反(假继承)而 改为独立类(e2m2e/algorithm/forces/force_model.py:30-38)。

消费面:System 出了这个包,被谁读

分离是否承重,取决于 System 被多少不构造 Dynamics 的代码消费。逐个核实如下。

力模型侧:读 coordinate_system、spice、origin、gravitational_parameter

ForceModel 持有 system(类型标注就是 Any),构造时强制 system.coordinate_system 已设置(force_model.py:45-58);传播时把每个力模型 序列化为 Rust 元组(force.to_rust_spec(self.system)),并读 system.origin 作为 observer 传入(force_model.py:281-293);system.spice 是否存在被用作 "资源缺失还是能力缺失"的分流依据(force_model.py:226-233)。它从不构造 Dynamics,也明确不继承 Dynamics(force_model.py:30-38)。

PhysicalModel._resolve_mu(system) 是力模型侧对 System 最直接的消费:显式 μ 缺失时调 system.gravitational_parameter(self._body) (e2m2e/algorithm/forces/physical_model.py:29-41)。ConicalShadowModel. flux_factor(t, state, system)require_inertial_frame 从 system 取出 coordinate_system、spice、原点三件事,再查太阳与遮挡体位置算光照份额 (e2m2e/algorithm/forces/shadow.py:166-197;physical_model.py:69-84)。

转移与求解侧:读 origin 与 gravitational_parameter

低推力三件套都把 system 当参数包用。qlaw_guess(system, ...) 只从中解析中心体 μ:先查力模型里的 PointMassGravity,查不到再 system.gravitational_parameter(origin)origin 本身用 getattr 兜底成 "EARTH"(e2m2e/algorithm/transfer/qlaw.py:210、265-282)。 LowThrustShootingLowThrustCollocation 构造时各做两件事:把力模型 to_rust_spec(system) 预序列化,把 origin 存为 observer (e2m2e/algorithm/transfer/lowthrust_shooting.py:125-162; e2m2e/algorithm/transfer/lowthrust_collocation.py:59-88)。

NormalFormContext 从 system 只取 μ,且是 getattr(system, "mu", None) 探测: CR3BP_System 有就取,没有就回退固化常量 (e2m2e/algorithm/normal_form/context.py:64、114、196-201)。

坐标转换侧:读 mu、特征尺度、spice

SynodicJ2000System 持有 CR3BP_System 与 spice:逐点转换读 mu(质心平移) 与 characteristic_time(时间量纲化),批量路径把 mu 与时间单位两个标量传给 Rust(e2m2e/algorithm/coordinate/synodic_j2000.py:28-41、52、96-97)。 rho_bridge 的一组函数以 EphemerisSystem 为参:读 system.spice 构造 SynodicAxes、读 system.get_body_state("MOON", et) 取月球状态 (e2m2e/algorithm/coordinate/rho_bridge.py:47-63、110)。

轨道保持与预报:构造并配置 EphemerisSystem

control_orbit 构造十体 EphemerisSystem、赋 coordinate_system,然后交给 蒙特卡洛流程(e2m2e/algorithm/station_keeping/controller.py:228-247);工作进程里 按参数重建同一个系统(e2m2e/algorithm/station_keeping/monte_carlo.py:644-650), 另建一个仅带特征尺度的 CR3BP_System 供会合系转换用(monte_carlo.py:66-72、 707-716)。propagate_orbit 同样先构造 EphemerisSystem、补 coordinate_system, 再 ForceModel.from_config(force_config, system)(propagation.py:118-124)。 这两个模块消费 System,但传播由 ForceModel 完成,不经过 Dynamics。

其余散点

PoincareSection.periapsis(center, system)mu 与主/次天体名来定截面中心 (e2m2e/algorithm/manifold/sections.py:202-228)。design_orbit 里同一个 CR3BP system 实例同时喂给四路消费:构造 CR3BP_Dynamics、直接调 get_jacobi_constant、读 characteristic_time 做时间换算、构造 SynodicJ2000System 做坐标转换(design_orbit.py:983-987、1030-1032)。

按 System 抽象签名的多态函数

签名标注 system: System 的入口有:PhysicalModel._resolve_mu / to_rust_spec(physical_model.py:29、45)、ConicalShadowModel.flux_factor (shadow.py:170)、qlaw_guessmake_shooter_for_qlaw(qlaw.py:210、290)、 LowThrustShooting.__init__LowThrustCollocation.__init__ (lowthrust_shooting.py:125;lowthrust_collocation.py:59)、 NormalFormContext.__init__(normal_form/context.py:64)。

把这些入口实际读到的成员摊开看,真正对 CR3BP 与星历两种系统都成立的多态只 剩基类那一项:gravitational_parameter,它在 CR3BP 侧接受 "primary"/"secondary" 返回无量纲值,星历侧接受 SPICE 天体名返回 km³/s²(system.py:42-51 的 docstring 把这个双语义写明)。其余入口虽标着 System,实际读的都是实现侧成员 (origincoordinate_systemspicemu),靠 getattr/hasattr 兜底。 契约测试把双系统同一接口这件事固定下来:同一组断言跑在 CR3BP_System 与 EphemerisSystem 两个实现上(tests/algorithm/dynamics/test_system_contract.py:53-64)。

鸭子类型:名义 System,实际结构

类型标注之下,真实契约比注解更薄,三处实证:

  • 数据层的 Orbit/OrbitFamily 把 system 存为 Any,docstring 明说"数据层 不依赖算法层",只用 hasattr(system, "get_jacobi_constant") 判断能力 (e2m2e/data/types/orbit.py:8-12、50、418-429)。

  • 低推力测试直接传 SimpleNamespace(origin="EARTH") 当 system 用 (tests/algorithm/transfer/test_lowthrust_collocation.py:23; tests/algorithm/transfer/test_qlaw_failure.py:37)。

  • RelativeDynamics.linear_model 用 try/except 在 compute_jacobian_A(t, state)compute_jacobian_A(state) 两种签名间适配 (e2m2e/algorithm/proximity/relative_dynamics.py:145-150)。

propagate 内部:一次调用里数据怎么动

入口编排

Dynamics.propagate 本体(dynamics.py:144-233)按固定次序处理输入:

  1. initial_state 转 ndarray,max_step 经钩子取得(dynamics.py:199-200)。

  2. 事件规范化:单个 callable 包成列表;空列表等价于无事件,直接置 None (dynamics.py:201-205)。

  3. 若开碰撞检测,先把碰撞事件造出来并入事件列表,同时检查初始状态是否已在 某天体半径内(dynamics.py:207-209)。

  4. backend 校验:只许 "scipy"/"rust",有事件时必须显式给,否则报错 (dynamics.py:211-214)。

  5. 初始即在半径内的,短路返回单点轨迹加即时碰撞标记:scipy 事件不会对 g<0 的起点触发,必须显式处理(dynamics.py:217-221、474-493)。

  6. with_stm 分发到两支(dynamics.py:223-229)。

  7. 若开碰撞检测,从结果的事件段提取碰撞信息(dynamics.py:231-232、495-510)。

scipy 与 Rust 两条后端路径

scipy 路径(基类实现):_propagate_state_only_get_eom_func(False) 的 ODE 右端调 solve_ivpresult.y 转置成 (n, 6);失败或空结果抛 PropagationFailure,不允许拿空轨迹伪装成功(dynamics.py:291-325)。 _propagate_with_stm 先把 6×6 单位阵展平拼成 42 维增广状态再积分,回来按 前 6 维/后 36 维拆开(dynamics.py:254-271)。两条 scipy 路径都在返回前写 last_trajectory(含 STM 时连 last_stm)(dynamics.py:273-274、327)。

Rust 路径(子类覆写):无事件时三个子类都要求 Rust 扩展可用,缺失即抛错、 不降级 scipy。传过 FFI 的东西逐类不同:

  • CR3BP:只有 mu 一个系统参数,加时间区间、t_eval、初值、容差、步长 (dynamics.py:851-859;STM 版 dynamics.py:782-790)。

  • BCR4BP:mu 加太阳四参数 mu_sun/sun_distance/sun_angular_rate/ sun_phase0(bcr4bp_dynamics.py:360-370;STM 版 293-303)。

  • 星历:bodiesorigingm_values 三个从 system 现取的序列 (ephemeris_dynamics.py:124-126、133-142)。天体位置不经过 Python 对象传递: Rust 侧在积分内环直接查进程内 SPICE(先查星历缓存,未命中回退 cspice) (crates/e2m2e-spice/src/spk_accel.rs:46-56)。在这条链上,EphemerisSystem 的角色收敛为一个参数包。

Rust 路径回来后都做长度防御校验(返回点数不等于请求点数即抛错),再写 last_trajectory(dynamics.py:792-804、861-870)。

事件时的第三支:有事件且 backend="rust" 时,CR3BP/BCR4BP 走通用 Rust 积分器 solve_ivp_events,ODE 右端仍以 Python 回调形式传入,事件函数被 折算成 (g, terminal, direction) 三元组(dynamics.py:700-756、721-723; bcr4bp_dynamics.py:225-271)。EphemerisDynamics 不支持事件:events 非 None 直接 NotImplementedError(ephemeris_dynamics.py:85-113、162-189)。

返回字典的键在何时出现

time/states 恒有。其余键的出现条件:

  • stmwith_stm=True 时(dynamics.py:276-280)。

  • status/cause:只在纯状态路径出现:基类 scipy 版与 CR3BP 的两个 Rust 纯状态分支(dynamics.py:329-334、872-877、926-931)。STM 路径、 EphemerisDynamics 与 BCR4BP_Dynamics 的 Rust 分支都不带这两个键。

  • jacobi/jacobi_errorwith_jacobi=True 且为 CR3BP 时。基类 _handle_jacobi 是 no-op(dynamics.py:345-358),CR3BP_Dynamics 覆写为逐点 计算并写缓存(dynamics.py:1012-1022),BCR4BP 覆写为抛错 (bcr4bp_dynamics.py:461-463)。

  • t_events/y_events:传了 events 时,逐事件的触发时刻与状态 (dynamics.py:282-284、336-338)。

  • collisioncollision_detection=True 时,未碰撞为 None,否则是 {"body", "t", "state"}(dynamics.py:231-232、507-509)。

事件与碰撞检测

事件函数是 scipy 语义的 g(t, state) -> float,可挂 terminal/direction 属性;PoincareSection.event(...) 是主要的构造者 (tests/algorithm/dynamics/test_events.py:21-26)。with_stm=True 时事件函数 收到的是 42 维增广状态(test_events.py:86-97)。

碰撞检测把"撞天体"翻译成终端事件:_collision_specs 从 system 读 primary_radius_km/secondary_radius_km(都没注入则报错)、读 mu 定两天体 在会合系的固定位置 [-μ,0,0] 与 [1-μ,0,0](dynamics.py:398-428); _setup_collision_detection 再读 DU 把半径从 km 折成无量纲,构造 g = |r - center| - R 的 terminal 事件并追加到用户事件之后 (dynamics.py:447-472)。碰撞事件在事件列表末尾这一顺序被 _extract_collision 反向利用:按"列表尾部 n 个"索引回每个天体的触发记录 (dynamics.py:501-509)。这就是碰撞数据流全程读 system 的三个字段:半径、mu、 DU。

三条传播链的差异

  • CR3BP:无量纲、自治。EOM 不显含 t;时间区间、t_eval 都是无量纲量; 系统侧只贡献 mu

  • BCR4BP:在 CR3BP 右端上叠加太阳直接项与间接项,太阳位置由 system.sun_position(t) 解析给出,EOM 显式含时 (bcr4bp_dynamics.py:72-121)。雅可比左下块在伪势能 Hessian 上再加太阳项 偏导(bcr4bp_dynamics.py:123-159)。无 Jacobi 积分。

  • 星历:物理单位(km、km/s、et 秒)、含时。EOM 对每个天体查 GM、对非原点 天体查星历位置,原点天体出中心项、其余出第三体摄动加间接项 (ephemeris_dynamics.py:235-293、256-291)。max_step 默认 60 秒且按传播 时长自适应收紧(ephemeris_dynamics.py:65-83)。Python 侧单步算加速度要逐 天体过 SPICE;Rust 快速路径把这个查询挪进 Rust 内环,Python 只递参数包。

这份分离保护了什么

一个 System 服务多个 Dynamics 与别的消费者

System 实例被共享的实证在测试与生产两侧都有。测试侧,BCR4BP 对照实验把坐标 转换器持有的 system 直接拿来再建一个动力学: CR3BP_Dynamics(spice_syn_j2000.cr3bp_system) (tests/algorithm/dynamics/test_bcr4bp_model.py:168)。生产侧,design_orbit 里 同一个 system 实例同时喂动力学、Jacobi 计算、时间换算、坐标转换四路 (design_orbit.py:983-1032);StabilityAnalysis 与不变流形各自从 orbit 上挂的 system 重建自己的 CR3BP_Dynamics(stability.py:95-98; e2m2e/algorithm/manifold/manifolds.py:114)。反向的共享同样成立:同一 earth_moon_system fixture 上既可以挂 earth_moon_dynamics (tests/conftest.py:13-25),也可以被任何只要系统参数的测试直接消费。

与之对照,Dynamics 的配置是按任务改的:同一个搜索任务族内,dynamics 构造后 被覆写 integrator/rtol/atol/max_step(search_parallel.py:837-842)。若配置留在 System 上,共享 system 的消费者之间会互相踩配置;分离让"模型参数"与"这一次 怎么积"各有其主。

多态缝与契约测试

System ABC 是 CR3BP 与星历两个世界之间唯一的类型级接缝,缝宽三个成员 (system.py:15-51)。消费面普查显示:真正跨缝多态的调用几乎都收敛到 gravitational_parameter 一项,其余访问都是 getattr/hasattr 探测实现侧成员。 两个契约测试文件把这个格局固定下来:test_system_contract.py 对两个实现跑 同一组接口断言(tests/algorithm/dynamics/test_system_contract.py:53-64), test_dynamics_contract.py 对 propagate 的输出形状断言 (n, 6) 与 (n, 6, 6) (tests/algorithm/dynamics/test_dynamics_contract.py:28-38)。

生命周期的差异

System 的寿命跟着数据走:Orbit 持有 system 引用,序列化再加载后引用还在 (orbit.py:50、253、275),稳定性分析、流形计算可以随时从它再长出 dynamics。 Dynamics 的寿命跟着任务走:构造、改配置、传播、读缓存,然后被丢弃; _corrected_dro_cacheddro_corrector 两个 fixture 各自建独立的 system+dynamics 对,互不共享(tests/algorithm/conftest.py:62-88)。两类对象的 生命周期差一个量级,这是分离在运行时最直观的体现。

两侧的真实重叠

分离并不彻底,有三处已知的重叠,各有来由:

  • Jacobi 常数双入口CR3BP_System.get_jacobi_constant 是定义所在 (cr3bp_system.py:319-354);CR3BP_Dynamics.compute_jacobi_constant 一行委托 (dynamics.py:1001-1010),供 _handle_jacobi 在传播后逐点调用。只想要 Jacobi 值、不想碰传播的调用方走 system 侧(design_orbit.py:987; e2m2e/algorithm/family/axial_initial_guess.py:166)。测试断言两入口数值一致 (tests/algorithm/dynamics/test_cr3bp_model.py:66-69)。

  • A 矩阵两处构造CR3BP_System.compute_stability_index 内部拼一份 6×6 线性化矩阵用于平动点特征值分析(cr3bp_system.py:418-423); CR3BP_Dynamics.compute_jacobian_A 拼同构矩阵供 STM 变分方程与延拓模块 复用(dynamics.py:611-638)。两者共用 pseudo_potential_hessian (e2m2e/algorithm/dynamics/potential.py:14-58),Hessian 只有一份,拼装各 归各的语境:一个在"系统性质"里,一个在"传播配套"里。

  • 碰撞半径与 DU 的跨层读取。碰撞检测是 Dynamics 的职责,但半径存在 System 上、折无量纲要用 System 的 DU(dynamics.py:398-428、447-472)。 半径是天体属性而非积分配置,故数据留在 system,事件构造留在 dynamics。

附:文件地图

  • System 侧:system.py(ABC)、cr3bp_system.py(CR3BP_System + LibrationPoint)、ephemeris_system.py(EphemerisSystem)、 bcr4bp_system.py(BCR4BPSystem)。

  • Dynamics 侧:dynamics.py(Dynamics 基类 + CR3BP_Dynamics + propagate_state_at_orbit_time)、ephemeris_dynamics.py (EphemerisDynamics)、bcr4bp_dynamics.py(BCR4BP_Dynamics)。

  • 共享:potential.py(伪势能 Hessian,供 CR3BP/BCR4BP 的雅可比与平动点 稳定性分析共用)。

  • 行为写照:tests/algorithm/dynamics/(契约、事件、碰撞、变分方程)。